diff --git a/code/admin/package-lock.json b/code/admin/package-lock.json index 30d04cf..adde86d 100644 --- a/code/admin/package-lock.json +++ b/code/admin/package-lock.json @@ -15,12 +15,15 @@ "@mui/material": "^6.1.10", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-router": "^7.0.2" + "react-error-boundary": "^4.1.2", + "react-router": "^7.0.2", + "systemjs": "^6.15.1" }, "devDependencies": { "@eslint/js": "^9.15.0", "@types/react": "^19.0.1", "@types/react-dom": "^19.0.1", + "@types/systemjs": "^6.15.1", "@typescript-eslint/eslint-plugin": "^8.17.0", "@typescript-eslint/parser": "^8.17.0", "@vitejs/plugin-react": "^4.3.4", @@ -1755,6 +1758,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.1.tgz", + "integrity": "sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "undici-types": "~6.20.0" + } + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -1795,6 +1810,13 @@ "@types/react": "*" } }, + "node_modules/@types/systemjs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/systemjs/-/systemjs-6.15.1.tgz", + "integrity": "sha512-MfDFIN+jRQOX1JRBrbbb72tsFJnK0n7mtLC+L2Y3t7As/vFxJiFGA/09FE+6ssFheHAibd8Q3gs959c+Sgf/9A==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.17.0.tgz", @@ -4617,6 +4639,18 @@ "react": "^19.0.0" } }, + "node_modules/react-error-boundary": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz", + "integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -5109,6 +5143,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/systemjs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/systemjs/-/systemjs-6.15.1.tgz", + "integrity": "sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5289,6 +5329,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", diff --git a/code/admin/package.json b/code/admin/package.json index e3a0d56..e4f7bb0 100644 --- a/code/admin/package.json +++ b/code/admin/package.json @@ -18,12 +18,15 @@ "@mui/material": "^6.1.10", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-router": "^7.0.2" + "react-error-boundary": "^4.1.2", + "react-router": "^7.0.2", + "systemjs": "^6.15.1" }, "devDependencies": { "@eslint/js": "^9.15.0", "@types/react": "^19.0.1", "@types/react-dom": "^19.0.1", + "@types/systemjs": "^6.15.1", "@typescript-eslint/eslint-plugin": "^8.17.0", "@typescript-eslint/parser": "^8.17.0", "@vitejs/plugin-react": "^4.3.4", diff --git a/code/admin/src/Pages/Dashboard/Dashboard.tsx b/code/admin/src/Pages/Dashboard/Dashboard.tsx index 4ed6483..fcd1905 100644 --- a/code/admin/src/Pages/Dashboard/Dashboard.tsx +++ b/code/admin/src/Pages/Dashboard/Dashboard.tsx @@ -1,10 +1,97 @@ import Box from "@mui/material/Box"; +import {createElement, Fragment, ReactElement, Suspense, useEffect, useState} from "react"; +import {ErrorBoundary} from "react-error-boundary"; import {Alert} from "@mui/material"; +import Module = System.Module; + +interface PluginDto { + readonly Name: string; +} + +interface WidgetData { + readonly url: string; + readonly element: ReactElement; +} export default function Dashboard() { + const [widgets, setWidgets] = useState([]); + + useEffect(() => { + async function getWidgets() { + const plugins = await getPlugins(); + const widgets: WidgetData[] = []; + + for (const plugin of plugins) { + const url: string = `/api/v1/plugins/${plugin.Name}/assets/widget.umd.js`; + + try { + const module: Module = await System.import(url); + const element: ReactElement = createElement(module.default); + + widgets.push({ + url: url, + element: element, + }); + } catch (error) { + console.error(error); + } + } + + if (lock) { + return; + } + setWidgets(widgets); + } + + let lock: boolean = false; + + getWidgets(); + return () => { + lock = true; + }; + }, []); + + useEffect(() => { + + }, [widgets]); + + const getPlugins = async (): Promise => { + const response: Response = await fetch('/api/v1/plugins'); + + if (!response.ok) { + return []; + } + return await response.json() as PluginDto[]; + } + return ( - Work in progress + { + return ( + + Error while rendering widgets: {error.message} + + ); + } + }> + {widgets.map((widget, i) => { + return ( + + + + Loading widgets... + + }> + {widget.element} + + + + ); + })} + ); } diff --git a/code/admin/src/main.tsx b/code/admin/src/main.tsx index 51ec7b2..d84f611 100644 --- a/code/admin/src/main.tsx +++ b/code/admin/src/main.tsx @@ -1,8 +1,14 @@ -import {StrictMode} from 'react'; +import React, {StrictMode} from 'react'; +import * as MaterialUI from '@mui/material'; import {createRoot} from 'react-dom/client'; import './index.css'; import App from './App.tsx'; import {BrowserRouter} from "react-router"; +import 'systemjs'; + +window.React = React; +// @ts-expect-error should declare in global +window.MaterialUI = MaterialUI; createRoot(document.getElementById('root')!).render( diff --git a/code/admin/vite.config.ts b/code/admin/vite.config.ts index 8b0f57b..cea672b 100644 --- a/code/admin/vite.config.ts +++ b/code/admin/vite.config.ts @@ -1,7 +1,34 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import {defineConfig} from 'vite'; +import react from '@vitejs/plugin-react'; // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], -}) + plugins: [ + react() + ], + server: { + proxy: { + '/api': 'http://localhost:11778' + }, + }, + optimizeDeps: { + include: [ + 'react', + '@mui/material' + ] + }, + build: { + rollupOptions: { + external: [ + 'react', + '@mui/material' + ], + output: { + globals: { + 'react': 'React', + '@mui/material': 'MUI', + } + } + } + } +}); diff --git a/code/server/loader/Systems/WebApi.cs b/code/server/loader/Systems/WebApi.cs index 8211677..483f08e 100644 --- a/code/server/loader/Systems/WebApi.cs +++ b/code/server/loader/Systems/WebApi.cs @@ -68,7 +68,12 @@ private void RegisterAssets(WebServer server) server.WithStaticFolder( baseRoute: $"/api/v1/plugins/{name}/assets/", fileSystemPath: path, - isImmutable: true); +#if DEBUG + isImmutable: false, +#else + isImmutable: true, +#endif + configure: m => { m.AddCustomMimeType(".umd.js", "application/javascript"); }); } } @@ -104,4 +109,4 @@ private Task HandleListPlugins(IHttpContext context) #endregion } -} \ No newline at end of file +} diff --git a/code/server/scripting/EmoteSystem/Admin/.gitignore b/code/server/scripting/EmoteSystem/Admin/.gitignore new file mode 100644 index 0000000..763301f --- /dev/null +++ b/code/server/scripting/EmoteSystem/Admin/.gitignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ \ No newline at end of file diff --git a/code/server/scripting/EmoteSystem/Admin/index.ts b/code/server/scripting/EmoteSystem/Admin/index.ts new file mode 100644 index 0000000..df2458b --- /dev/null +++ b/code/server/scripting/EmoteSystem/Admin/index.ts @@ -0,0 +1,3 @@ +import {Widget} from './src/widget'; + +export default Widget; \ No newline at end of file diff --git a/code/server/scripting/EmoteSystem/Admin/package-lock.json b/code/server/scripting/EmoteSystem/Admin/package-lock.json index 658aeea..3eeda06 100644 --- a/code/server/scripting/EmoteSystem/Admin/package-lock.json +++ b/code/server/scripting/EmoteSystem/Admin/package-lock.json @@ -7,7 +7,6 @@ "": { "name": "emotesystem", "version": "1.0.0", - "hasInstallScript": true, "license": "MIT", "dependencies": { "@emotion/react": "^11.13.5", diff --git a/code/server/scripting/EmoteSystem/Admin/package.json b/code/server/scripting/EmoteSystem/Admin/package.json index 61abfbb..1ba4c42 100644 --- a/code/server/scripting/EmoteSystem/Admin/package.json +++ b/code/server/scripting/EmoteSystem/Admin/package.json @@ -3,9 +3,7 @@ "version": "1.0.0", "license": "MIT", "author": "Rayshader", - "main": "widget.tsx", "scripts": { - "postinstall": "vite build", "dev": "vite", "start": "vite", "build": "vite build" diff --git a/code/server/scripting/EmoteSystem/Admin/widget.tsx b/code/server/scripting/EmoteSystem/Admin/src/widget.tsx similarity index 61% rename from code/server/scripting/EmoteSystem/Admin/widget.tsx rename to code/server/scripting/EmoteSystem/Admin/src/widget.tsx index c0f9aa6..1a52c05 100644 --- a/code/server/scripting/EmoteSystem/Admin/widget.tsx +++ b/code/server/scripting/EmoteSystem/Admin/src/widget.tsx @@ -1,39 +1,46 @@ import {Alert, Card, CardActionArea, Tooltip, Typography} from "@mui/material"; -import {useEffect, useState} from "react"; +import {useEffect, useRef, useState} from "react"; -export default function Widget() { +interface EmoteDto { + readonly Username: string; + readonly Emote: string; +} + +export function Widget() { const [username, setUsername] = useState(''); const [emote, setEmote] = useState(''); + const load = useRef(false); - useEffect(() => handleRefresh(), [username, emote]); + useEffect(() => { + if (load.current) { + return; + } + load.current = true; + return handleRefresh(); + }, []); // NOTE: start an interval to automatically fetch last emote every X s/m/h. // @ts-ignore - const getLastEmote = async () => { - const response = await fetch('/api/v1/plugins/emote/'); + const getLastEmote = async (): Promise => { + const response: Response = await fetch('/api/v1/plugins/emote/'); if (!response.ok) { return; } - const json = await response.json(); - - return { - username: json.Username ?? '', - emote: json.Emote ?? '', - }; + return await response.json() as EmoteDto | undefined; }; const handleRefresh = () => { // @ts-ignore async function request() { - const data = await getLastEmote(); + const data: EmoteDto | undefined = await getLastEmote(); - if (lock) { + if (!data || lock) { return; } - setUsername(data.username); - setEmote(data.emote); + setUsername(data.Username); + setEmote(data.Emote); } let lock: boolean = false; @@ -48,7 +55,7 @@ export default function Widget() { return ( - + {isEmpty() ? diff --git a/code/server/scripting/EmoteSystem/Admin/vite.config.js b/code/server/scripting/EmoteSystem/Admin/vite.config.js index 43ef985..c4bc56e 100644 --- a/code/server/scripting/EmoteSystem/Admin/vite.config.js +++ b/code/server/scripting/EmoteSystem/Admin/vite.config.js @@ -2,27 +2,31 @@ import react from '@vitejs/plugin-react'; export default defineConfig({ + define: { + 'process.env.NODE_ENV': '"production"', + }, + plugins: [ + react(), + ], build: { - target: 'modules', lib: { - name: 'emote', - entry: './widget.tsx', - formats: ['es'], + name: 'widget', + entry: './index.ts', + formats: ['umd'], fileName: 'widget', }, rollupOptions: { - // NOTE: Exclude dependencies already declared in Admin webapp. - // This will reduce the bundle of your widget with only the - // minimum required. external: [ - '@emotion/react', - '@emotion/styled', - '@mui/icons-material', - '@mui/material', 'react', - 'react-dom' + '@mui/material', ], - }, + output: { + name: 'Widget', + globals: { + 'react': 'React', + '@mui/material': 'MaterialUI', + } + } + } }, - plugins: [react()], }); diff --git a/code/server/scripting/EmoteSystem/EmoteSystem.csproj b/code/server/scripting/EmoteSystem/EmoteSystem.csproj index 9e50e80..78fbc4f 100644 --- a/code/server/scripting/EmoteSystem/EmoteSystem.csproj +++ b/code/server/scripting/EmoteSystem/EmoteSystem.csproj @@ -11,7 +11,7 @@ - + assets\%(RecursiveDir)%(Filename)%(Extension) PreserveNewest @@ -27,5 +27,6 @@ +