Port popup to React; fix backend port colliding with Vite dev server
Popup is now a real React app (App.tsx/main.tsx via createRoot), wired through @wxt-dev/module-react. tsconfig.json needed an explicit jsx: "react-jsx" -- WXT's generated .wxt/tsconfig.json doesn't set it, so tsc --noEmit failed on JSX syntax even though the Vite build itself was fine. Root cause of the actually-reported bug (blank popup, predating the React port too): the backend defaulted to port 3000, the same port WXT's dev server uses for this extension. With both running, the popup's script tags pointed at the Vite dev server but the backend answered instead, so main.tsx never loaded -- "View Page Source" showed raw unbundled dev-mode HTML pointing at localhost:3000. Backend now binds :8080 (see backend commit), and BACKEND_HTTP_URL/BACKEND_WS_URL + host_permissions here follow it. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01YXiHuScXrjxBh7yFGAPq3B
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { browser } from 'wxt/browser';
|
||||
import { DEFAULT_CONNECTION_STATUS, DEFAULT_LOCK_STATE, type ConnectionStatus, type LockState } from '@/shared/types';
|
||||
import type { NexaMessage } from '@/shared/messaging';
|
||||
|
||||
const CONNECTION_LABELS: Record<ConnectionStatus, string> = {
|
||||
connecting: 'Connecting…',
|
||||
connected: 'Connected',
|
||||
disconnected: 'Disconnected — retrying',
|
||||
'auth-error': 'Sign-in failed — retrying',
|
||||
};
|
||||
|
||||
export function App() {
|
||||
const [lockState, setLockState] = useState<LockState>(DEFAULT_LOCK_STATE);
|
||||
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>(DEFAULT_CONNECTION_STATUS);
|
||||
|
||||
useEffect(() => {
|
||||
browser.runtime
|
||||
.sendMessage({ type: 'nexa:get-lock-state' } satisfies NexaMessage)
|
||||
.then((state: LockState | undefined) => setLockState(state ?? DEFAULT_LOCK_STATE));
|
||||
|
||||
browser.runtime
|
||||
.sendMessage({ type: 'nexa:get-connection-status' } satisfies NexaMessage)
|
||||
.then((status: ConnectionStatus | undefined) => setConnectionStatus(status ?? DEFAULT_CONNECTION_STATUS));
|
||||
|
||||
const listener = (message: NexaMessage) => {
|
||||
if (message?.type === 'nexa:lock-state-changed') {
|
||||
setLockState(message.state);
|
||||
} else if (message?.type === 'nexa:connection-status-changed') {
|
||||
setConnectionStatus(message.status);
|
||||
}
|
||||
};
|
||||
browser.runtime.onMessage.addListener(listener);
|
||||
return () => browser.runtime.onMessage.removeListener(listener);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header>
|
||||
<h1>Nexa</h1>
|
||||
<p className="subtitle">Trading companion</p>
|
||||
</header>
|
||||
|
||||
<section className="connection">
|
||||
<span className={`connection-dot connection-${connectionStatus}`} />
|
||||
{' '}
|
||||
{CONNECTION_LABELS[connectionStatus]}
|
||||
</section>
|
||||
|
||||
<section className="status">
|
||||
{lockState.locked ? (
|
||||
<>
|
||||
<div className="badge badge-locked">🔒 Locked</div>
|
||||
<p className="reason">{lockState.reason ?? 'No reason provided.'}</p>
|
||||
<p className="scope">Scope: {lockState.scope}</p>
|
||||
</>
|
||||
) : (
|
||||
<div className="badge badge-unlocked">Unlocked</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<a
|
||||
className="thresholds-link"
|
||||
href="#"
|
||||
title="Threshold settings are not part of this milestone"
|
||||
>
|
||||
Adjust loss thresholds →
|
||||
</a>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import { browser } from 'wxt/browser';
|
||||
import { DEFAULT_CONNECTION_STATUS, DEFAULT_LOCK_STATE, type ConnectionStatus, type LockState } from '@/shared/types';
|
||||
import type { NexaMessage } from '@/shared/messaging';
|
||||
|
||||
const app = document.getElementById('app');
|
||||
if (!app) throw new Error('Popup root element #app not found.');
|
||||
|
||||
app.innerHTML = `
|
||||
<header>
|
||||
<h1>Nexa</h1>
|
||||
<p class="subtitle">Trading companion</p>
|
||||
</header>
|
||||
|
||||
<section class="connection" id="connection"></section>
|
||||
<section class="status" id="status"></section>
|
||||
|
||||
<a class="thresholds-link" id="thresholds-link" href="#" title="Threshold settings are not part of this milestone">
|
||||
Adjust loss thresholds →
|
||||
</a>
|
||||
`;
|
||||
|
||||
const connectionEl = document.getElementById('connection') as HTMLElement;
|
||||
const statusEl = document.getElementById('status') as HTMLElement;
|
||||
|
||||
function escapeHtml(value: string): string {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = value;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
const CONNECTION_LABELS: Record<ConnectionStatus, string> = {
|
||||
connecting: 'Connecting…',
|
||||
connected: 'Connected',
|
||||
disconnected: 'Disconnected — retrying',
|
||||
'auth-error': 'Sign-in failed — retrying',
|
||||
};
|
||||
|
||||
function renderConnection(status: ConnectionStatus): void {
|
||||
connectionEl.innerHTML = `<span class="connection-dot connection-${status}"></span> ${CONNECTION_LABELS[status]}`;
|
||||
}
|
||||
|
||||
function renderStatus(state: LockState): void {
|
||||
statusEl.innerHTML = state.locked
|
||||
? `<div class="badge badge-locked">🔒 Locked</div>
|
||||
<p class="reason">${escapeHtml(state.reason ?? 'No reason provided.')}</p>
|
||||
<p class="scope">Scope: ${escapeHtml(state.scope)}</p>`
|
||||
: `<div class="badge badge-unlocked">Unlocked</div>`;
|
||||
}
|
||||
|
||||
async function loadLockState(): Promise<LockState> {
|
||||
const message: NexaMessage = { type: 'nexa:get-lock-state' };
|
||||
const state = (await browser.runtime.sendMessage(message)) as LockState | undefined;
|
||||
return state ?? DEFAULT_LOCK_STATE;
|
||||
}
|
||||
|
||||
async function loadConnectionStatus(): Promise<ConnectionStatus> {
|
||||
const message: NexaMessage = { type: 'nexa:get-connection-status' };
|
||||
const status = (await browser.runtime.sendMessage(message)) as ConnectionStatus | undefined;
|
||||
return status ?? DEFAULT_CONNECTION_STATUS;
|
||||
}
|
||||
|
||||
loadLockState().then(renderStatus);
|
||||
loadConnectionStatus().then(renderConnection);
|
||||
|
||||
browser.runtime.onMessage.addListener((message: NexaMessage) => {
|
||||
if (message?.type === 'nexa:lock-state-changed') {
|
||||
renderStatus(message.state);
|
||||
} else if (message?.type === 'nexa:connection-status-changed') {
|
||||
renderConnection(message.status);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { App } from './App';
|
||||
|
||||
const root = document.getElementById('app');
|
||||
if (!root) throw new Error('Popup root element #app not found.');
|
||||
|
||||
createRoot(root).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
Reference in New Issue
Block a user