diff --git a/pkg/model/policy.go b/pkg/model/policy.go index d63a5a6..7f0f03b 100644 --- a/pkg/model/policy.go +++ b/pkg/model/policy.go @@ -17,7 +17,7 @@ type ScriptPolicy struct { // Hash of the source code that leads to this policy. // If this is a replacement mapping, the original source may not be pointed to by any policy. // The original source should still exist in the scripts table, which can be located by the same hash. - FromScriptHash int64 // postgres does not support unsigned integers, so we have to pretend + FromScriptHash int64 `gorm:"uniqueIndex"` // postgres does not support unsigned integers, so we have to pretend // The ID of the replacement source (ScriptPolicyReplace) // or verbatim source (ScriptPolicyAllowed) // or 0 (other) diff --git a/pkg/model/script.go b/pkg/model/script.go index f2d7ade..2239a51 100644 --- a/pkg/model/script.go +++ b/pkg/model/script.go @@ -26,7 +26,7 @@ func HashParse(hash string) (uint64, error){ type Script struct { ID int64 `gorm:"primaryKey"` Name string - Hash int64 // postgres does not support unsigned integers, so we have to pretend + Hash int64 `gorm:"uniqueIndex"` // postgres does not support unsigned integers, so we have to pretend Source string ResourceType ResourceType // is this a submission or is it a mapfix ResourceID int64 // which submission / mapfix did this script first appear in diff --git a/pkg/web_api/scripts.go b/pkg/web_api/scripts.go index 8e30499..70247fe 100644 --- a/pkg/web_api/scripts.go +++ b/pkg/web_api/scripts.go @@ -36,10 +36,28 @@ func (svc *Service) CreateScript(ctx context.Context, req *api.ScriptCreate) (*a return nil, err } + hash := int64(model.HashSource(req.Source)) + + // Check if a script with this hash already exists + filter := service.NewScriptFilter() + filter.SetHash(hash) + existingScripts, err := svc.inner.ListScripts(ctx, filter, model.Page{Number: 1, Size: 1}) + if err != nil { + return nil, err + } + + // If script with this hash exists, return existing script ID + if len(existingScripts) > 0 { + return &api.ScriptID{ + ScriptID: existingScripts[0].ID, + }, nil + } + + // Create new script script, err := svc.inner.CreateScript(ctx, model.Script{ ID: 0, Name: req.Name, - Hash: int64(model.HashSource(req.Source)), + Hash: hash, Source: req.Source, ResourceType: model.ResourceType(req.ResourceType), ResourceID: req.ResourceID.Or(0), diff --git a/web/bun.lock b/web/bun.lock index 7aa1b58..bb009db 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -7,6 +7,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@monaco-editor/react": "^4.7.0", "@mui/icons-material": "^7.3.6", "@mui/material": "^7.3.6", "@tanstack/react-query": "^5.90.12", @@ -185,6 +186,10 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@monaco-editor/loader": ["@monaco-editor/loader@1.7.0", "", { "dependencies": { "state-local": "^1.0.6" } }, "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA=="], + + "@monaco-editor/react": ["@monaco-editor/react@4.7.0", "", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="], + "@mui/core-downloads-tracker": ["@mui/core-downloads-tracker@7.3.6", "", {}, "sha512-QaYtTHlr8kDFN5mE1wbvVARRKH7Fdw1ZuOjBJcFdVpfNfRYKF3QLT4rt+WaB6CKJvpqxRsmEo0kpYinhH5GeHg=="], "@mui/icons-material": ["@mui/icons-material@7.3.6", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "@mui/material": "^7.3.6", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-0FfkXEj22ysIq5pa41A2NbcAhJSvmcZQ/vcTIbjDsd6hlslG82k5BEBqqS0ZJprxwIL3B45qpJ+bPHwJPlF7uQ=="], @@ -305,6 +310,8 @@ "@types/react-transition-group": ["@types/react-transition-group@4.4.12", "", { "peerDependencies": { "@types/react": "*" } }, "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], + "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], @@ -365,6 +372,8 @@ "dom-helpers": ["dom-helpers@5.2.1", "", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="], + "dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="], + "electron-to-chromium": ["electron-to-chromium@1.5.266", "", {}, "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg=="], "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="], @@ -477,10 +486,14 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + "marked": ["marked@14.0.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ=="], + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "monaco-editor": ["monaco-editor@0.55.1", "", { "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" } }, "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], @@ -563,6 +576,8 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], "stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], diff --git a/web/package-lock.json b/web/package-lock.json index cd59153..e12434f 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@monaco-editor/react": "^4.7.0", "@mui/icons-material": "^7.3.6", "@mui/material": "^7.3.6", "@tanstack/react-query": "^5.90.12", @@ -1148,6 +1149,29 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.7.0.tgz", + "integrity": "sha512-gIwR1HrJrrx+vfyOhYmCZ0/JcWqG5kbfG7+d3f/C1LXk2EvzAbHSg3MQ5lO2sMlo9izoAZ04shohfKLVT6crVA==", + "license": "MIT", + "dependencies": { + "state-local": "^1.0.6" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz", + "integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==", + "license": "MIT", + "dependencies": { + "@monaco-editor/loader": "^1.5.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@mui/core-downloads-tracker": { "version": "7.3.6", "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.6.tgz", @@ -2531,6 +2555,16 @@ "csstype": "^3.0.2" } }, + "node_modules/dompurify": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", + "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.267", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", @@ -3202,6 +3236,19 @@ "yallist": "^3.0.2" } }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -3229,6 +3276,17 @@ "node": "*" } }, + "node_modules/monaco-editor": { + "version": "0.55.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", + "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", + "license": "MIT", + "peer": true, + "dependencies": { + "dompurify": "3.2.7", + "marked": "14.0.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3742,6 +3800,12 @@ "node": ">=0.10.0" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==", + "license": "MIT" + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", diff --git a/web/package.json b/web/package.json index d8c4fcf..aaf83cd 100644 --- a/web/package.json +++ b/web/package.json @@ -12,6 +12,7 @@ "dependencies": { "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", + "@monaco-editor/react": "^4.7.0", "@mui/icons-material": "^7.3.6", "@mui/material": "^7.3.6", "@tanstack/react-query": "^5.90.12", diff --git a/web/src/App.tsx b/web/src/App.tsx index 9afe728..26c9037 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -16,6 +16,7 @@ import AdminSubmitPage from '@/app/admin-submit/page' import OperationPage from '@/app/operations/[operationId]/page' import ReviewerDashboardPage from '@/app/reviewer-dashboard/page' import UserDashboardPage from '@/app/user-dashboard/page' +import ScriptReviewPage from '@/app/script-review/page' import NotFound from '@/app/not-found/page' function App() { @@ -35,6 +36,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/web/src/app/reviewer-dashboard/page.tsx b/web/src/app/reviewer-dashboard/page.tsx index 00a663a..b25ce1d 100644 --- a/web/src/app/reviewer-dashboard/page.tsx +++ b/web/src/app/reviewer-dashboard/page.tsx @@ -26,6 +26,8 @@ import NavigateNextIcon from "@mui/icons-material/NavigateNext"; import { useTitle } from "@/app/hooks/useTitle"; import AssignmentIcon from "@mui/icons-material/Assignment"; import BuildIcon from "@mui/icons-material/Build"; +import CodeIcon from "@mui/icons-material/Code"; +import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; interface TabPanelProps { children?: React.ReactNode; @@ -169,6 +171,8 @@ export default function ReviewerDashboardPage() { const [isLoadingMapfixes, setIsLoadingMapfixes] = useState(false); const [tabValue, setTabValue] = useState(0); const [userRoles, setUserRoles] = useState(null); + const [scriptPoliciesCount, setScriptPoliciesCount] = useState(0); + const [isLoadingScripts, setIsLoadingScripts] = useState(false); // Fetch user roles useEffect(() => { @@ -361,6 +365,45 @@ export default function ReviewerDashboardPage() { return () => controller.abort(); }, [userRoles]); + // Fetch script policies needing review + useEffect(() => { + const controller = new AbortController(); + + async function fetchScriptPolicies() { + if (!userRoles || !hasRole(userRoles, RolesConstants.ScriptWrite)) return; + + setIsLoadingScripts(true); + try { + const res = await fetch( + '/v1/script-policy?Page=1&Limit=50&Policy=0', + { signal: controller.signal } + ); + + if (!res.ok) { + console.error('Failed to fetch script policies:', res.status); + setIsLoadingScripts(false); + return; + } + + const policies = await res.json(); + // The API does not provide total count, so we use the length of returned policies + setScriptPoliciesCount(Array.isArray(policies) ? policies.length : 0); + } catch (error) { + if (!(error instanceof DOMException && error.name === 'AbortError')) { + console.error("Error fetching script policies:", error); + } + } finally { + setIsLoadingScripts(false); + } + } + + if (userRoles) { + fetchScriptPolicies(); + } + + return () => controller.abort(); + }, [userRoles]); + const handleTabChange = (_event: React.SyntheticEvent, newValue: number) => { setTabValue(newValue); }; @@ -414,6 +457,7 @@ export default function ReviewerDashboardPage() { hasRole(userRoles, RolesConstants.MapfixUpload) || hasRole(userRoles, RolesConstants.ScriptWrite) ); + const canReviewScripts = hasRole(userRoles, RolesConstants.ScriptWrite); if (!hasAnyReviewerRole(userRoles)) { return ( @@ -470,7 +514,7 @@ export default function ReviewerDashboardPage() { {/* Summary Cards */} @@ -521,6 +565,43 @@ export default function ReviewerDashboardPage() { )} + + {canReviewScripts && ( + + + + + + + + {isLoadingScripts ? ( + + ) : ( + scriptPoliciesCount >= 50 ? '50+' : scriptPoliciesCount + )} + + + Scripts Pending Review + + + + + + + + )} {/* Tabs */} diff --git a/web/src/app/script-review/page.tsx b/web/src/app/script-review/page.tsx new file mode 100644 index 0000000..914b685 --- /dev/null +++ b/web/src/app/script-review/page.tsx @@ -0,0 +1,1165 @@ +import { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import Editor, { DiffEditor } from "@monaco-editor/react"; + +// MUI Components +import { + Typography, + Box, + Snackbar, + Alert, + LinearProgress, + Container, + CircularProgress, +} from "@mui/material"; + +import Webpage from "@/app/_components/webpage"; + +// MUI Icons +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import BlockIcon from '@mui/icons-material/Block'; +import DeleteIcon from '@mui/icons-material/Delete'; +import FindReplaceIcon from '@mui/icons-material/FindReplace'; +import CodeIcon from '@mui/icons-material/Code'; +import WarningAmberIcon from '@mui/icons-material/WarningAmber'; +import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; +import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; +import HomeIcon from '@mui/icons-material/Home'; +import DeleteForeverIcon from '@mui/icons-material/DeleteForever'; +import SendIcon from '@mui/icons-material/Send'; + +import { ErrorDisplay } from "@/app/_components/ErrorDisplay"; +import { ScriptPolicy, PolicyType, PolicyLabels } from "@/app/ts/ScriptPolicy"; +import { Script } from "@/app/ts/Script"; +import { useTitle } from "@/app/hooks/useTitle"; +import { useUser } from "@/app/hooks/useUser"; +import { RolesConstants, hasRole } from "@/app/ts/Roles"; + +interface SnackbarState { + open: boolean; + message: string | null; + severity: 'success' | 'error' | 'info' | 'warning'; +} + +// IDE Button Component +const IDEButton = ({ + children, + onClick, + disabled, + variant = 'default', + fullWidth = false, + icon, +}: { + children: React.ReactNode; + onClick?: () => void; + disabled?: boolean; + variant?: 'default' | 'primary' | 'success' | 'error' | 'warning'; + fullWidth?: boolean; + icon?: React.ReactNode; +}) => { + const getColors = () => { + switch (variant) { + case 'primary': + return { + bg: '#0e639c', + hoverBg: '#1177bb', + activeBg: '#007acc', + color: '#ffffff', + border: '#007acc', + }; + case 'success': + return { + bg: '#0e7e0e', + hoverBg: '#0f9d0f', + activeBg: '#14b814', + color: '#ffffff', + border: '#14b814', + }; + case 'error': + return { + bg: '#7e0e0e', + hoverBg: '#9d0f0f', + activeBg: '#b81414', + color: '#ffffff', + border: '#b81414', + }; + case 'warning': + return { + bg: '#7e5e0e', + hoverBg: '#9d750f', + activeBg: '#b88614', + color: '#ffffff', + border: '#b88614', + }; + default: + return { + bg: 'transparent', + hoverBg: 'rgba(255, 255, 255, 0.08)', + activeBg: 'rgba(255, 255, 255, 0.12)', + color: '#cccccc', + border: '#3e3e42', + }; + } + }; + + const colors = getColors(); + + return ( + + ); +}; + +// IDE Info Badge Component +const InfoBadge = ({ + children, + type = 'info', + icon, +}: { + children: React.ReactNode; + type?: 'info' | 'warning' | 'error' | 'success'; + icon?: React.ReactNode; +}) => { + const getColors = () => { + switch (type) { + case 'warning': + return { bg: 'rgba(250, 200, 90, 0.15)', border: '#fac85a', color: '#fac85a' }; + case 'error': + return { bg: 'rgba(240, 82, 82, 0.15)', border: '#f05252', color: '#f05252' }; + case 'success': + return { bg: 'rgba(80, 200, 120, 0.15)', border: '#50c878', color: '#50c878' }; + default: + return { bg: 'rgba(100, 150, 230, 0.15)', border: '#6496e6', color: '#6496e6' }; + } + }; + + const colors = getColors(); + + return ( +
+ {icon && {icon}} + {children} +
+ ); +}; + +export default function ScriptReviewPage() { + const navigate = useNavigate(); + const { user, isLoading: userLoading } = useUser(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [scriptPolicy, setScriptPolicy] = useState(null); + const [script, setScript] = useState