diff --git a/Controlzmo/Controlzmo.csproj b/Controlzmo/Controlzmo.csproj index 0e36df7..22c2beb 100644 --- a/Controlzmo/Controlzmo.csproj +++ b/Controlzmo/Controlzmo.csproj @@ -31,6 +31,10 @@ 1701;1702;CA1416 + + + + diff --git a/Controlzmo/Hubs/TestyHub.cs b/Controlzmo/Hubs/TestyHub.cs new file mode 100644 index 0000000..69f6702 --- /dev/null +++ b/Controlzmo/Hubs/TestyHub.cs @@ -0,0 +1,36 @@ +using Lombok.NET; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Threading.Tasks; +using System.Timers; + +namespace Controlzmo.Hubs +{ + [Component] + public partial class Ticker : CreateOnStartup { + private int count = 0; + private readonly IHubContext hub; + + public Ticker(IServiceProvider sp) { + hub = sp.GetRequiredService>(); + var timer = new Timer(1000); + timer.Elapsed += Tick; + timer.Start(); + } + + private void Tick(object? sender, ElapsedEventArgs args) { + hub.Clients.All.SendAsync("ToBrowser", count++, "foo"); + } + } + + [RequiredArgsConstructor] + public partial class TestyHub : Hub + { + public async Task FromBrowser(string a, int b) + { +Console.Error.WriteLine($"Got message from browser with {a} and {b}"); + await Task.CompletedTask; + } + } +} diff --git a/Controlzmo/Pages/Overhead.cshtml b/Controlzmo/Pages/Overhead.cshtml deleted file mode 100644 index 36baded..0000000 --- a/Controlzmo/Pages/Overhead.cshtml +++ /dev/null @@ -1,25 +0,0 @@ -@page "/overhead" -@{ - ViewData["Title"] = "Overhead"; -} - -
- - - - - - diff --git a/Controlzmo/Pages/Shared/_Layout.cshtml b/Controlzmo/Pages/Shared/_Layout.cshtml index b1ae8f7..f299537 100644 --- a/Controlzmo/Pages/Shared/_Layout.cshtml +++ b/Controlzmo/Pages/Shared/_Layout.cshtml @@ -17,11 +17,9 @@ - - + - @RenderSection("Scripts", required: false) diff --git a/Controlzmo/Pages/Testy.cshtml b/Controlzmo/Pages/Testy.cshtml new file mode 100644 index 0000000..6aeba33 --- /dev/null +++ b/Controlzmo/Pages/Testy.cshtml @@ -0,0 +1,7 @@ +@page "/testy" +@{ + ViewData["Title"] = "Testy"; +} + +
To be replaced by React app
+ diff --git a/Controlzmo/Startup.cs b/Controlzmo/Startup.cs index 5ffd24b..3a80345 100644 --- a/Controlzmo/Startup.cs +++ b/Controlzmo/Startup.cs @@ -77,6 +77,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { endpoints.MapRazorPages(); endpoints.MapHub("/hub/connectzmo"); + endpoints.MapHub("/hub/testy"); }); app.ApplicationServices.GetServices(); diff --git a/Controlzmo/react/.gitignore b/Controlzmo/react/.gitignore new file mode 100644 index 0000000..504afef --- /dev/null +++ b/Controlzmo/react/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +package-lock.json diff --git a/Controlzmo/react/eslint.config.js b/Controlzmo/react/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/Controlzmo/react/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/Controlzmo/react/package.json b/Controlzmo/react/package.json new file mode 100644 index 0000000..4594597 --- /dev/null +++ b/Controlzmo/react/package.json @@ -0,0 +1,31 @@ +{ + "name": "react", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@microsoft/signalr": "^6.0.5", + "react": "^19.2.0", + "react-dom": "^19.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^24.10.1", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.1", + "eslint": "^9.39.1", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.4.24", + "globals": "^16.5.0", + "typescript": "~5.9.3", + "typescript-eslint": "^8.46.4", + "vite": "^7.2.4" + } +} diff --git a/Controlzmo/react/src/Testy.tsx b/Controlzmo/react/src/Testy.tsx new file mode 100644 index 0000000..6cfdedd --- /dev/null +++ b/Controlzmo/react/src/Testy.tsx @@ -0,0 +1,53 @@ +import { HubConnection, HubConnectionBuilder } from '@microsoft/signalr'; +import { useEffect, useState } from 'react' + +// https://stackoverflow.com/a/74603295 +function useLiveUpdates(connectionRef: HubConnection | undefined) { + useEffect(() => { + if (connectionRef) { + try { + connectionRef + .start() + .then(() => { +console.log('started'); +connectionRef.send('FromBrowser', "hello", 666); // If we don't actually trigger it it doesn't seem to get created... + }) + .catch((err) => { + console.error('cannot connect', err); + }); + } catch (error) { + console.error('cannot setup', error); + } + } + + return () => { + connectionRef?.stop(); +console.log('stopped'); + }; + }, [connectionRef]); +}; + +export default function Testy() { + const [connectionRef, setConnection] = useState(); + + useEffect(() => { + const con = new HubConnectionBuilder() + .withUrl('/hub/testy') + .withAutomaticReconnect() + .build(); + con.on('ToBrowser', function (a, b) { +console.log('got message', a, b); + con.send('FromBrowser', b, a); + }); + // eslint-disable-next-line react-hooks/set-state-in-effect + setConnection(con); + }, []); + + useLiveUpdates(connectionRef); + + const [count, setCount] = useState(0); + return ( + ); +} diff --git a/Controlzmo/react/src/main.tsx b/Controlzmo/react/src/main.tsx new file mode 100644 index 0000000..69bf41d --- /dev/null +++ b/Controlzmo/react/src/main.tsx @@ -0,0 +1,11 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import Testy from './Testy.tsx' + +const root = document.getElementById('root')!; +const mode = root.dataset.mode!; +createRoot(root).render( + + {mode == 'testy' && } + , +) diff --git a/Controlzmo/react/tsconfig.app.json b/Controlzmo/react/tsconfig.app.json new file mode 100644 index 0000000..a9b5a59 --- /dev/null +++ b/Controlzmo/react/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/Controlzmo/react/tsconfig.json b/Controlzmo/react/tsconfig.json new file mode 100644 index 0000000..67d9d9c --- /dev/null +++ b/Controlzmo/react/tsconfig.json @@ -0,0 +1,6 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + ] +} diff --git a/Controlzmo/react/vite.config.ts b/Controlzmo/react/vite.config.ts new file mode 100644 index 0000000..51064df --- /dev/null +++ b/Controlzmo/react/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite'; +import plugin from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [plugin()], + server: { + port: 58938, + }, + root: 'src/', + build: { + outDir: '../../wwwroot/js/react', + emptyOutDir: true, + rollupOptions: { + input: 'src/main.tsx', + output: { + entryFileNames: 'bundle.js', + }, + }, + }, +}) diff --git a/Controlzmo/wwwroot/js/.gitignore b/Controlzmo/wwwroot/js/.gitignore new file mode 100644 index 0000000..414594d --- /dev/null +++ b/Controlzmo/wwwroot/js/.gitignore @@ -0,0 +1 @@ +react/