This commit is contained in:
2025-11-26 17:03:29 +01:00
parent ff3528e5ef
commit fef75008d8
147 changed files with 112370 additions and 5162 deletions

View File

@@ -1,295 +0,0 @@
<html><head><style type="text/css">
@keyframes gridpilot-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.85; transform: scale(1.03); }
}
@keyframes gridpilot-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes gridpilot-slide-in {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes gridpilot-checkered {
0% { background-position: 0 0; }
100% { background-position: 20px 20px; }
}
@keyframes gridpilot-progress {
0% { background-position: 0% 50%; }
100% { background-position: 100% 50%; }
}
#gridpilot-overlay {
position: fixed;
bottom: 20px;
right: 20px;
width: 340px;
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
z-index: 2147483647;
animation: gridpilot-slide-in 0.4s ease-out;
pointer-events: auto;
}
#gridpilot-overlay * {
box-sizing: border-box;
}
.gridpilot-card {
background: #12121B;
border-radius: 4px;
border: 1px solid rgba(183, 183, 187, 0.2);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
overflow: hidden;
}
.gridpilot-header {
background: linear-gradient(90deg, #c8102e 0%, #a00d25 100%);
padding: 10px 14px;
display: flex;
align-items: center;
gap: 10px;
position: relative;
overflow: hidden;
}
.gridpilot-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
linear-gradient(45deg, transparent 48%, rgba(255,255,255,0.05) 49%, rgba(255,255,255,0.05) 51%, transparent 52%),
linear-gradient(-45deg, transparent 48%, rgba(255,255,255,0.05) 49%, rgba(255,255,255,0.05) 51%, transparent 52%);
background-size: 8px 8px;
animation: gridpilot-checkered 1.5s linear infinite;
opacity: 0.5;
}
.gridpilot-logo {
font-size: 22px;
animation: gridpilot-pulse 2s ease-in-out infinite;
position: relative;
z-index: 1;
}
.gridpilot-title {
color: #ffffff;
font-size: 13px;
font-weight: 700;
letter-spacing: 1.5px;
text-transform: uppercase;
position: relative;
z-index: 1;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
flex: 1;
}
.gridpilot-btn {
background: rgba(255, 255, 255, 0.15);
color: #ffffff;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 3px;
padding: 4px 10px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
cursor: pointer;
position: relative;
z-index: 1;
transition: all 0.15s ease;
}
.gridpilot-btn:hover {
background: rgba(255, 255, 255, 0.25);
border-color: rgba(255, 255, 255, 0.5);
}
.gridpilot-btn:active {
background: rgba(255, 255, 255, 0.35);
transform: scale(0.97);
}
.gridpilot-btn.paused {
background: #4e4e57;
border-color: #ffffff;
color: #ffffff;
animation: gridpilot-pulse 1s ease-in-out infinite;
}
.gridpilot-close-btn {
background: rgba(200, 16, 46, 0.6);
border-color: rgba(200, 16, 46, 0.8);
}
.gridpilot-close-btn:hover {
background: rgba(200, 16, 46, 0.8);
border-color: #c8102e;
}
.gridpilot-close-btn:active {
background: #c8102e;
}
.gridpilot-header-buttons {
display: flex;
gap: 6px;
position: relative;
z-index: 1;
}
.gridpilot-body {
padding: 14px;
background: #1a1a24;
}
.gridpilot-status {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.gridpilot-spinner {
width: 22px;
height: 22px;
border: 2px solid rgba(200, 16, 46, 0.3);
border-top-color: #c8102e;
border-radius: 50%;
animation: gridpilot-spin 0.8s linear infinite;
flex-shrink: 0;
}
.gridpilot-spinner.paused {
animation-play-state: paused;
border-top-color: #777880;
border-color: rgba(119, 120, 128, 0.3);
}
.gridpilot-action-text {
color: rgba(255, 255, 255, 0.92);
font-size: 14px;
font-weight: 500;
line-height: 1.4;
}
.gridpilot-progress-container {
margin-bottom: 12px;
}
.gridpilot-progress-bar {
height: 4px;
background: rgba(78, 78, 87, 0.5);
border-radius: 2px;
overflow: hidden;
}
.gridpilot-progress-fill {
height: 100%;
background: linear-gradient(90deg, #c8102e, #e8304a, #c8102e);
background-size: 200% 100%;
animation: gridpilot-progress 2s linear infinite;
border-radius: 2px;
transition: width 0.4s ease-out;
}
.gridpilot-progress-fill.paused {
animation-play-state: paused;
background: #777880;
}
.gridpilot-step-info {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 6px;
}
.gridpilot-step-text {
color: rgba(255, 255, 255, 0.6);
font-size: 11px;
}
.gridpilot-step-count {
color: #c8102e;
font-size: 11px;
font-weight: 600;
}
.gridpilot-personality {
color: rgba(255, 255, 255, 0.5);
font-size: 11px;
font-style: italic;
text-align: center;
padding-top: 10px;
border-top: 1px solid rgba(183, 183, 187, 0.15);
}
.gridpilot-footer {
background: #12121B;
padding: 8px 14px;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
border-top: 1px solid rgba(183, 183, 187, 0.1);
}
.gridpilot-footer-text {
color: rgba(255, 255, 255, 0.4);
font-size: 10px;
letter-spacing: 0.5px;
}
.gridpilot-footer-dot {
width: 4px;
height: 4px;
background: #c8102e;
border-radius: 50%;
animation: gridpilot-pulse 1.5s ease-in-out infinite;
}
.gridpilot-footer-dot.paused {
background: #777880;
animation: none;
}
</style></head><body><div id="gridpilot-overlay">
<div class="gridpilot-card">
<div class="gridpilot-header">
<span class="gridpilot-logo">🏎️</span>
<span class="gridpilot-title">GridPilot</span>
<div class="gridpilot-header-buttons">
<button class="gridpilot-btn gridpilot-close-btn" id="gridpilot-close-btn" onclick="(function() {
window.__gridpilot_close_requested = true;
})()">Stop</button>
</div>
</div>
<div class="gridpilot-body">
<div class="gridpilot-status">
<div class="gridpilot-spinner"></div>
<span class="gridpilot-action-text" id="gridpilot-action">Processing step 0...</span>
</div>
<div class="gridpilot-progress-container">
<div class="gridpilot-progress-bar">
<div class="gridpilot-progress-fill" id="gridpilot-progress" style="width: 0%"></div>
</div>
<div class="gridpilot-step-info">
<span class="gridpilot-step-text" id="gridpilot-step-text">Processing step 0...</span>
<span class="gridpilot-step-count" id="gridpilot-step-count">Step 0 of 17</span>
</div>
</div>
<div class="gridpilot-personality" id="gridpilot-personality">🏁 Getting ready for the green flag...</div>
</div>
<div class="gridpilot-footer">
<div class="gridpilot-footer-dot"></div>
<span class="gridpilot-footer-text">Automating your session setup</span>
</div>
</div>
</div></body></html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,8 @@ import { StartAutomationSessionUseCase } from '@/packages/application/use-cases/
import { CheckAuthenticationUseCase } from '@/packages/application/use-cases/CheckAuthenticationUseCase';
import { InitiateLoginUseCase } from '@/packages/application/use-cases/InitiateLoginUseCase';
import { ClearSessionUseCase } from '@/packages/application/use-cases/ClearSessionUseCase';
import { loadAutomationConfig, getAutomationMode, AutomationMode } from '@/packages/infrastructure/config';
import { ConfirmCheckoutUseCase } from '@/packages/application/use-cases/ConfirmCheckoutUseCase';
import { loadAutomationConfig, getAutomationMode, AutomationMode, BrowserModeConfigLoader } from '@/packages/infrastructure/config';
import { PinoLogAdapter } from '@/packages/infrastructure/adapters/logging/PinoLogAdapter';
import { NoOpLogAdapter } from '@/packages/infrastructure/adapters/logging/NoOpLogAdapter';
import { loadLoggingConfig } from '@/packages/infrastructure/config/LoggingConfig';
@@ -16,6 +17,7 @@ import type { ISessionRepository } from '@/packages/application/ports/ISessionRe
import type { IScreenAutomation } from '@/packages/application/ports/IScreenAutomation';
import type { IAutomationEngine } from '@/packages/application/ports/IAutomationEngine';
import type { IAuthenticationService } from '@/packages/application/ports/IAuthenticationService';
import type { ICheckoutConfirmationPort } from '@/packages/application/ports/ICheckoutConfirmationPort';
import type { ILogger } from '@/packages/application/ports/ILogger';
export interface BrowserConnectionResult {
@@ -92,7 +94,11 @@ function getAdapterMode(envMode: AutomationMode): AutomationAdapterMode {
* @param logger - Logger instance for the adapter
* @returns PlaywrightAutomationAdapter instance (implements both IScreenAutomation and IAuthenticationService)
*/
function createBrowserAutomationAdapter(mode: AutomationMode, logger: ILogger): PlaywrightAutomationAdapter | MockBrowserAutomationAdapter {
function createBrowserAutomationAdapter(
mode: AutomationMode,
logger: ILogger,
browserModeConfigLoader: BrowserModeConfigLoader
): PlaywrightAutomationAdapter | MockBrowserAutomationAdapter {
const config = loadAutomationConfig();
// Resolve absolute template path for Electron environment
@@ -108,18 +114,28 @@ function createBrowserAutomationAdapter(mode: AutomationMode, logger: ILogger):
});
const adapterMode = getAdapterMode(mode);
logger.info('Creating browser automation adapter', { envMode: mode, adapterMode });
// Get browser mode configuration from provided loader
const browserModeConfig = browserModeConfigLoader.load();
logger.info('Creating browser automation adapter', {
envMode: mode,
adapterMode,
browserMode: browserModeConfig.mode,
browserModeSource: browserModeConfig.source,
});
switch (mode) {
case 'production':
case 'development':
return new PlaywrightAutomationAdapter(
{
headless: mode === 'production',
headless: browserModeConfig.mode === 'headless',
mode: adapterMode,
userDataDir: sessionDataPath,
},
logger.child({ adapter: 'Playwright', mode: adapterMode })
logger.child({ adapter: 'Playwright', mode: adapterMode }),
browserModeConfigLoader
);
case 'test':
@@ -139,7 +155,9 @@ export class DIContainer {
private checkAuthenticationUseCase: CheckAuthenticationUseCase | null = null;
private initiateLoginUseCase: InitiateLoginUseCase | null = null;
private clearSessionUseCase: ClearSessionUseCase | null = null;
private confirmCheckoutUseCase: ConfirmCheckoutUseCase | null = null;
private automationMode: AutomationMode;
private browserModeConfigLoader: BrowserModeConfigLoader;
private constructor() {
// Initialize logger first - it's needed by other components
@@ -153,8 +171,15 @@ export class DIContainer {
const config = loadAutomationConfig();
// Initialize browser mode config loader as singleton
this.browserModeConfigLoader = new BrowserModeConfigLoader();
this.sessionRepository = new InMemorySessionRepository();
this.browserAutomation = createBrowserAutomationAdapter(config.mode, this.logger);
this.browserAutomation = createBrowserAutomationAdapter(
config.mode,
this.logger,
this.browserModeConfigLoader
);
this.automationEngine = new MockAutomationEngineAdapter(
this.browserAutomation,
this.sessionRepository
@@ -241,6 +266,21 @@ export class DIContainer {
return null;
}
public setConfirmCheckoutUseCase(
checkoutConfirmationPort: ICheckoutConfirmationPort
): void {
// Create ConfirmCheckoutUseCase with checkout service from browser automation
// and the provided confirmation port
this.confirmCheckoutUseCase = new ConfirmCheckoutUseCase(
this.browserAutomation as any, // implements ICheckoutService
checkoutConfirmationPort
);
}
public getConfirmCheckoutUseCase(): ConfirmCheckoutUseCase | null {
return this.confirmCheckoutUseCase;
}
/**
* Initialize automation connection based on mode.
* In production/development mode, connects via Playwright browser automation.
@@ -292,6 +332,14 @@ export class DIContainer {
this.logger.info('DIContainer shutdown complete');
}
/**
* Get the browser mode configuration loader.
* Provides access to runtime browser mode control (headed/headless).
*/
public getBrowserModeConfigLoader(): BrowserModeConfigLoader {
return this.browserModeConfigLoader;
}
/**
* Reset the singleton instance (useful for testing with different configurations).
*/

View File

@@ -4,6 +4,7 @@ import { DIContainer } from './di-container';
import type { HostedSessionConfig } from '@/packages/domain/entities/HostedSessionConfig';
import { StepId } from '@/packages/domain/value-objects/StepId';
import { AuthenticationState } from '@/packages/domain/value-objects/AuthenticationState';
import { ElectronCheckoutConfirmationAdapter } from '@/packages/infrastructure/adapters/ipc/ElectronCheckoutConfirmationAdapter';
let progressMonitorInterval: NodeJS.Timeout | null = null;
@@ -14,6 +15,10 @@ export function setupIpcHandlers(mainWindow: BrowserWindow): void {
const automationEngine = container.getAutomationEngine();
const logger = container.getLogger();
// Setup checkout confirmation adapter and wire it into the container
const checkoutConfirmationAdapter = new ElectronCheckoutConfirmationAdapter(mainWindow);
container.setConfirmCheckoutUseCase(checkoutConfirmationAdapter);
// Authentication handlers
ipcMain.handle('auth:check', async () => {
try {
@@ -21,11 +26,10 @@ export function setupIpcHandlers(mainWindow: BrowserWindow): void {
const checkAuthUseCase = container.getCheckAuthenticationUseCase();
if (!checkAuthUseCase) {
logger.warn('Authentication not available in mock mode');
logger.error('Authentication use case not available');
return {
success: true,
state: AuthenticationState.AUTHENTICATED,
message: 'Mock mode - authentication bypassed'
success: false,
error: 'Authentication not available - check system configuration'
};
}
@@ -301,4 +305,36 @@ export function setupIpcHandlers(mainWindow: BrowserWindow): void {
};
}
});
// Browser mode control handlers
ipcMain.handle('browser-mode:get', async () => {
try {
const loader = container.getBrowserModeConfigLoader();
if (process.env.NODE_ENV === 'development') {
return { mode: loader.getDevelopmentMode(), isDevelopment: true };
}
return { mode: 'headless', isDevelopment: false };
} catch (error) {
const err = error instanceof Error ? error : new Error('Unknown error');
logger.error('Failed to get browser mode', err);
return { mode: 'headless', isDevelopment: false };
}
});
ipcMain.handle('browser-mode:set', async (_event: IpcMainInvokeEvent, mode: 'headed' | 'headless') => {
try {
if (process.env.NODE_ENV === 'development') {
const loader = container.getBrowserModeConfigLoader();
loader.setDevelopmentMode(mode);
logger.info('Browser mode updated', { mode });
return { success: true, mode };
}
logger.warn('Browser mode change requested but not in development mode');
return { success: false, error: 'Only available in development mode' };
} catch (error) {
const err = error instanceof Error ? error : new Error('Unknown error');
logger.error('Failed to set browser mode', err);
return { success: false, error: err.message };
}
});
}

View File

@@ -20,6 +20,17 @@ export interface AuthActionResponse {
error?: string;
}
export interface CheckoutConfirmationRequest {
price: string;
state: 'ready' | 'insufficient_funds';
sessionMetadata: {
sessionName: string;
trackId: string;
carIds: string[];
};
timeoutMs: number;
}
export interface ElectronAPI {
startAutomation: (config: HostedSessionConfig) => Promise<{
success: boolean;
@@ -37,6 +48,12 @@ export interface ElectronAPI {
initiateLogin: () => Promise<AuthActionResponse>;
confirmLogin: () => Promise<AuthActionResponse>;
logout: () => Promise<AuthActionResponse>;
// Browser Mode APIs
getBrowserMode: () => Promise<{ mode: 'headed' | 'headless'; isDevelopment: boolean }>;
setBrowserMode: (mode: 'headed' | 'headless') => Promise<{ success: boolean; mode?: string; error?: string }>;
// Checkout Confirmation APIs
onCheckoutConfirmationRequest: (callback: (request: CheckoutConfirmationRequest) => void) => () => void;
confirmCheckout: (decision: 'confirmed' | 'cancelled' | 'timeout') => void;
}
contextBridge.exposeInMainWorld('electronAPI', {
@@ -56,4 +73,18 @@ contextBridge.exposeInMainWorld('electronAPI', {
initiateLogin: () => ipcRenderer.invoke('auth:login'),
confirmLogin: () => ipcRenderer.invoke('auth:confirmLogin'),
logout: () => ipcRenderer.invoke('auth:logout'),
// Browser Mode APIs
getBrowserMode: () => ipcRenderer.invoke('browser-mode:get'),
setBrowserMode: (mode: 'headed' | 'headless') => ipcRenderer.invoke('browser-mode:set', mode),
// Checkout Confirmation APIs
onCheckoutConfirmationRequest: (callback: (request: CheckoutConfirmationRequest) => void) => {
const listener = (_event: any, request: CheckoutConfirmationRequest) => callback(request);
ipcRenderer.on('checkout:request-confirmation', listener);
return () => {
ipcRenderer.removeListener('checkout:request-confirmation', listener);
};
},
confirmCheckout: (decision: 'confirmed' | 'cancelled' | 'timeout') => {
ipcRenderer.send('checkout:confirm', decision);
},
} as ElectronAPI);

View File

@@ -5,7 +5,7 @@
"main": "dist/main/main.cjs",
"type": "module",
"scripts": {
"dev": "unset ELECTRON_RUN_AS_NODE && electron-vite dev",
"dev": "NODE_ENV=development unset ELECTRON_RUN_AS_NODE && electron-vite dev",
"build": "electron-vite build",
"preview": "unset ELECTRON_RUN_AS_NODE && electron-vite preview",
"start": "unset ELECTRON_RUN_AS_NODE && electron ."

View File

@@ -2,6 +2,9 @@ import React, { useState, useEffect, useCallback } from 'react';
import { SessionCreationForm } from './components/SessionCreationForm';
import { SessionProgressMonitor } from './components/SessionProgressMonitor';
import { LoginPrompt } from './components/LoginPrompt';
import { BrowserModeToggle } from './components/BrowserModeToggle';
import { CheckoutConfirmationDialog } from './components/CheckoutConfirmationDialog';
import { RaceCreationSuccessScreen } from './components/RaceCreationSuccessScreen';
import type { HostedSessionConfig } from '../../../packages/domain/entities/HostedSessionConfig';
interface SessionProgress {
@@ -24,6 +27,26 @@ export function App() {
const [isRunning, setIsRunning] = useState(false);
const [loginStatus, setLoginStatus] = useState<LoginStatus>('idle');
const [checkoutRequest, setCheckoutRequest] = useState<{
price: string;
state: 'ready' | 'insufficient_funds';
sessionMetadata: {
sessionName: string;
trackId: string;
carIds: string[];
};
timeoutMs: number;
} | null>(null);
const [raceCreationResult, setRaceCreationResult] = useState<{
sessionId: string;
sessionName: string;
trackId: string;
carIds: string[];
finalPrice: string;
createdAt: Date;
} | null>(null);
const handleLogin = useCallback(async () => {
if (!window.electronAPI) return;
@@ -91,6 +114,11 @@ export function App() {
}
};
// Subscribe to checkout confirmation requests
const unsubscribeCheckout = window.electronAPI.onCheckoutConfirmationRequest((request) => {
setCheckoutRequest(request);
});
checkAuth();
window.electronAPI.onSessionProgress((newProgress: SessionProgress) => {
@@ -101,6 +129,11 @@ export function App() {
setIsRunning(false);
}
});
// Cleanup subscription on unmount
return () => {
unsubscribeCheckout?.();
};
}, []);
const handleStartAutomation = async (config: HostedSessionConfig) => {
@@ -157,6 +190,16 @@ export function App() {
);
}
// Show checkout confirmation dialog if requested
if (checkoutRequest) {
return <CheckoutConfirmationDialog request={checkoutRequest} />;
}
// Show race creation success screen if completed
if (raceCreationResult) {
return <RaceCreationSuccessScreen result={raceCreationResult} />;
}
if (authState !== 'AUTHENTICATED') {
return (
<LoginPrompt
@@ -178,37 +221,42 @@ export function App() {
<div style={{
flex: 1,
padding: '2rem',
borderRight: '1px solid #333'
borderRight: '1px solid #333',
display: 'flex',
flexDirection: 'column'
}}>
<h1 style={{ marginBottom: '2rem', color: '#fff' }}>
GridPilot Companion
</h1>
<p style={{ marginBottom: '2rem', color: '#aaa' }}>
Hosted Session Automation POC
</p>
<div style={{ flex: 1 }}>
<h1 style={{ marginBottom: '2rem', color: '#fff' }}>
GridPilot Companion
</h1>
<p style={{ marginBottom: '2rem', color: '#aaa' }}>
Hosted Session Automation POC
</p>
<SessionCreationForm
onSubmit={handleStartAutomation}
disabled={isRunning}
/>
{isRunning && (
<button
onClick={handleStopAutomation}
style={{
marginTop: '1rem',
padding: '0.75rem 1.5rem',
backgroundColor: '#dc3545',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '1rem',
fontWeight: 'bold'
}}
>
Stop Automation
</button>
)}
<SessionCreationForm
onSubmit={handleStartAutomation}
disabled={isRunning}
/>
{isRunning && (
<button
onClick={handleStopAutomation}
style={{
marginTop: '1rem',
padding: '0.75rem 1.5rem',
backgroundColor: '#dc3545',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '1rem',
fontWeight: 'bold'
}}
>
Stop Automation
</button>
)}
</div>
<BrowserModeToggle />
</div>
<div style={{
flex: 1,

View File

@@ -0,0 +1,40 @@
import { useEffect, useState } from 'react';
export function BrowserModeToggle() {
const [mode, setMode] = useState<'headed' | 'headless'>('headed');
const [isDevelopment, setIsDevelopment] = useState(false);
useEffect(() => {
window.electronAPI.getBrowserMode().then(({ mode, isDevelopment }) => {
setMode(mode);
setIsDevelopment(isDevelopment);
});
}, []);
if (!isDevelopment) return null;
const handleToggle = async () => {
const newMode = mode === 'headed' ? 'headless' : 'headed';
const result = await window.electronAPI.setBrowserMode(newMode);
if (result.success) {
setMode(newMode);
}
};
return (
<div style={{ padding: '10px', borderTop: '1px solid #333' }}>
<label style={{ color: '#aaa', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '8px' }}>
<input
type="checkbox"
checked={mode === 'headless'}
onChange={handleToggle}
style={{ cursor: 'pointer' }}
/>
Headless Browser Mode
</label>
<div style={{ fontSize: '0.85rem', color: '#666', marginTop: '4px', marginLeft: '24px' }}>
{mode === 'headless' ? 'Browser runs in background' : 'Browser window visible'}
</div>
</div>
);
}

View File

@@ -0,0 +1,116 @@
/**
* CheckoutConfirmationDialog component
* Displays checkout information and requests user confirmation before proceeding.
*/
import React, { useEffect, useState } from 'react';
interface CheckoutConfirmationRequest {
price: string;
state: 'ready' | 'insufficient_funds';
sessionMetadata: {
sessionName: string;
trackId: string;
carIds: string[];
};
timeoutMs: number;
}
interface CheckoutConfirmationDialogProps {
request: CheckoutConfirmationRequest;
}
export const CheckoutConfirmationDialog: React.FC<CheckoutConfirmationDialogProps> = ({
request,
}) => {
const [remainingSeconds, setRemainingSeconds] = useState(
Math.floor(request.timeoutMs / 1000)
);
useEffect(() => {
// Countdown timer
const intervalId = setInterval(() => {
setRemainingSeconds((prev) => {
if (prev <= 1) {
clearInterval(intervalId);
window.electronAPI.confirmCheckout('timeout');
return 0;
}
return prev - 1;
});
}, 1000);
return () => clearInterval(intervalId);
}, []);
const handleConfirm = () => {
window.electronAPI.confirmCheckout('confirmed');
};
const handleCancel = () => {
window.electronAPI.confirmCheckout('cancelled');
};
return (
<div className="checkout-confirmation-dialog">
<div className="dialog-overlay">
<div className="dialog-content">
<h2>Confirm Checkout</h2>
<div className="checkout-details">
<div className="price-section">
<span className="price-label">Price:</span>
<span className="price-value">{request.price}</span>
</div>
{request.state === 'insufficient_funds' && (
<div className="warning-section">
<span className="warning-text"> Insufficient funds</span>
</div>
)}
<div className="session-info">
<div className="info-row">
<span className="info-label">Session:</span>
<span className="info-value">{request.sessionMetadata.sessionName}</span>
</div>
<div className="info-row">
<span className="info-label">Track:</span>
<span className="info-value">{request.sessionMetadata.trackId}</span>
</div>
<div className="info-row">
<span className="info-label">Cars:</span>
<span className="info-value">
{request.sessionMetadata.carIds.join(', ')}
</span>
</div>
</div>
<div className="countdown-section">
<span className="countdown-text">
Time remaining: {remainingSeconds}s
</span>
</div>
</div>
<div className="dialog-actions">
<button
type="button"
className="btn btn-cancel"
onClick={handleCancel}
>
Cancel
</button>
<button
type="button"
className="btn btn-confirm"
onClick={handleConfirm}
>
Confirm
</button>
</div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,78 @@
/**
* RaceCreationSuccessScreen component
* Displays the successful race creation result with session details.
*/
import React from 'react';
interface RaceCreationResult {
sessionId: string;
sessionName: string;
trackId: string;
carIds: string[];
finalPrice: string;
createdAt: Date;
}
interface RaceCreationSuccessScreenProps {
result: RaceCreationResult;
}
export const RaceCreationSuccessScreen: React.FC<RaceCreationSuccessScreenProps> = ({
result,
}) => {
return (
<div className="race-creation-success-screen">
<div className="success-container">
<div className="success-header">
<h2> Race Created Successfully!</h2>
</div>
<div className="success-details">
<div className="detail-section">
<h3>Session Information</h3>
<div className="detail-row">
<span className="detail-label">Session Name:</span>
<span className="detail-value">{result.sessionName}</span>
</div>
<div className="detail-row">
<span className="detail-label">Session ID:</span>
<span className="detail-value">{result.sessionId}</span>
</div>
</div>
<div className="detail-section">
<h3>Track & Cars</h3>
<div className="detail-row">
<span className="detail-label">Track:</span>
<span className="detail-value">{result.trackId}</span>
</div>
<div className="detail-row">
<span className="detail-label">Cars:</span>
<span className="detail-value">{result.carIds.join(', ')}</span>
</div>
</div>
<div className="detail-section">
<h3>Financial</h3>
<div className="detail-row">
<span className="detail-label">Final Price:</span>
<span className="detail-value price">{result.finalPrice}</span>
</div>
</div>
<div className="detail-section">
<h3>Created</h3>
<div className="detail-row">
<span className="detail-label">Timestamp:</span>
<span className="detail-value">
{result.createdAt.toISOString().split('T')[0]} at{' '}
{result.createdAt.toLocaleTimeString()}
</span>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@@ -32,8 +32,7 @@ const STEP_NAMES: { [key: number]: string } = {
14: 'Set Time of Day',
15: 'Configure Weather',
16: 'Set Race Options',
17: 'Configure Team Driving',
18: 'Set Track Conditions'
17: 'Set Track Conditions'
};
export function SessionProgressMonitor({ sessionId, progress, isRunning }: SessionProgressMonitorProps) {
@@ -142,7 +141,7 @@ export function SessionProgressMonitor({ sessionId, progress, isRunning }: Sessi
)}
<div style={{ marginBottom: '1rem', color: '#aaa', fontSize: '14px' }}>
Progress: {progress.completedSteps.length} / 18 steps
Progress: {progress.completedSteps.length} / 17 steps
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>