design: redesign product demo UI

This commit is contained in:
root
2026-07-04 01:44:32 +02:00
parent 2d50cdb454
commit cb38cd0002
16 changed files with 1162 additions and 721 deletions

View File

@@ -1,28 +1,24 @@
import { useEffect, useMemo, useState } from 'react';
import Dashboard from './components/Dashboard';
import Header from './components/Header';
import HeroSection from './components/HeroSection';
import ProductDemoSection from './components/ProductDemoSection';
import ScenarioCards from './components/ScenarioCards';
import CriteriaCards from './components/CriteriaCards';
import EngineeringDetails from './components/EngineeringDetails';
import { DEMO_STEPS } from './data/demoSteps';
import { SCENARIOS } from './data/scenarios';
import { createSimulation, setRunning, stepSimulation, stepSimulationToNextState } from './domain/simulation';
import type { DemoStep, PresentationMode, ScenarioId, SimulationState } from './domain/types';
import GuidedDemoView from './components/GuidedDemoView';
const TICK_MS = 250;
function isFaultDemoStep(step: DemoStep): boolean {
return step.scenarioId === 'jam' || step.scenarioId === 'emergency_stop';
}
import { createSimulation, setRunning, stepSimulationToNextState } from './domain/simulation';
import type { ScenarioId, SimulationState } from './domain/types';
export default function App() {
const [activeScenarioId, setActiveScenarioId] = useState<ScenarioId>('normal_flow');
const [presentationMode, setPresentationMode] = useState<PresentationMode>('guided');
const [safeDemoEnabled, setSafeDemoEnabled] = useState(true);
const [demoStepIndex, setDemoStepIndex] = useState(0);
const [pendingFaultStepId, setPendingFaultStepId] = useState<string | undefined>();
const activeScenario = useMemo(
() => SCENARIOS.find((scenario) => scenario.id === activeScenarioId) ?? SCENARIOS[0],
() => SCENARIOS.find((s) => s.id === activeScenarioId) ?? SCENARIOS[0],
[activeScenarioId],
);
const currentDemoStep = DEMO_STEPS[demoStepIndex];
const [simulation, setSimulation] = useState<SimulationState>(() => createSimulation(activeScenario));
@@ -30,110 +26,70 @@ export default function App() {
setSimulation(createSimulation(activeScenario));
}, [activeScenario]);
useEffect(() => {
if (!simulation.running) {
return undefined;
// We are not using automatic interval in the new product demo by default to keep it step-by-step
// but if the user wants it to run automatically, we can preserve it if needed.
// Let's use handleNext to just step through.
const handleStartDemo = () => {
document.getElementById('demo')?.scrollIntoView({ behavior: 'smooth' });
if (!simulation.currentItem && demoStepIndex === 0) {
setSimulation(stepSimulationToNextState(simulation));
}
const timer = window.setInterval(() => {
setSimulation((current) => stepSimulation(current, TICK_MS));
}, TICK_MS);
return () => window.clearInterval(timer);
}, [simulation.running]);
const handleStart = () => setSimulation((current) => setRunning(current, true));
const handlePause = () => setSimulation((current) => setRunning(current, false));
const handleReset = () => setSimulation(createSimulation(activeScenario));
const handleStep = () => setSimulation((current) => stepSimulationToNextState(current));
const handleScenarioChange = (scenarioId: ScenarioId) => {
setPendingFaultStepId(undefined);
setActiveScenarioId(scenarioId);
};
const handleDemoStepChange = (nextIndex: number) => {
setPendingFaultStepId(undefined);
setDemoStepIndex(Math.max(0, Math.min(nextIndex, DEMO_STEPS.length - 1)));
};
const handleApplyDemoStepScenario = (step: DemoStep) => {
if (presentationMode === 'presentation' && safeDemoEnabled && isFaultDemoStep(step) && pendingFaultStepId !== step.id) {
setPendingFaultStepId(step.id);
return;
}
setPendingFaultStepId(undefined);
setActiveScenarioId(step.scenarioId);
};
const handleRunPreferredAction = (step: DemoStep) => {
if (activeScenarioId !== step.scenarioId) {
handleApplyDemoStepScenario(step);
return;
}
if (step.preferredAction === 'start') {
handleStart();
} else if (step.preferredAction === 'pause') {
handlePause();
} else if (step.preferredAction === 'reset') {
handleReset();
const handleNext = () => {
// If simulation is finished or errored, we might need to reset or move to next scenario
const isFinished = simulation.machineState === 'RETURN_HOME' || simulation.machineState === 'FAULT' || simulation.machineState === 'EMERGENCY_STOP';
if (isFinished) {
const nextIndex = (demoStepIndex + 1) % DEMO_STEPS.length;
setDemoStepIndex(nextIndex);
setActiveScenarioId(DEMO_STEPS[nextIndex].scenarioId);
} else {
handleStep();
setSimulation((current) => stepSimulationToNextState(current));
}
};
const handleRestartDemo = () => {
setPendingFaultStepId(undefined);
setDemoStepIndex(0);
setActiveScenarioId(DEMO_STEPS[0].scenarioId);
const handleReset = () => {
setSimulation(createSimulation(activeScenario));
};
return presentationMode === 'guided' ? (
<GuidedDemoView
simulation={simulation}
scenarios={SCENARIOS}
activeScenarioId={activeScenarioId}
presentationMode={presentationMode}
safeDemoEnabled={safeDemoEnabled}
demoSteps={DEMO_STEPS}
demoStepIndex={demoStepIndex}
currentDemoStep={currentDemoStep}
pendingFaultStepId={pendingFaultStepId}
onStart={handleStart}
onPause={handlePause}
onReset={handleReset}
onStep={handleStep}
onScenarioChange={handleScenarioChange}
onPresentationModeChange={setPresentationMode}
onSafeDemoChange={setSafeDemoEnabled}
onDemoStepChange={handleDemoStepChange}
onApplyDemoStepScenario={handleApplyDemoStepScenario}
onRunPreferredAction={handleRunPreferredAction}
onRestartDemo={handleRestartDemo}
/>
) : (
<Dashboard
simulation={simulation}
scenarios={SCENARIOS}
activeScenarioId={activeScenarioId}
presentationMode={presentationMode}
safeDemoEnabled={safeDemoEnabled}
demoSteps={DEMO_STEPS}
demoStepIndex={demoStepIndex}
currentDemoStep={currentDemoStep}
pendingFaultStepId={pendingFaultStepId}
onStart={handleStart}
onPause={handlePause}
onReset={handleReset}
onStep={handleStep}
onScenarioChange={handleScenarioChange}
onPresentationModeChange={setPresentationMode}
onSafeDemoChange={setSafeDemoEnabled}
onDemoStepChange={handleDemoStepChange}
onApplyDemoStepScenario={handleApplyDemoStepScenario}
onRunPreferredAction={handleRunPreferredAction}
onRestartDemo={handleRestartDemo}
/>
const handleScenarioChange = (scenarioId: ScenarioId) => {
setActiveScenarioId(scenarioId);
// Find first step that matches this scenario
const stepIdx = DEMO_STEPS.findIndex(s => s.scenarioId === scenarioId);
if (stepIdx >= 0) setDemoStepIndex(stepIdx);
document.getElementById('demo')?.scrollIntoView({ behavior: 'smooth' });
};
return (
<div className="product-page">
<Header simulation={simulation} />
<main className="product-main">
<HeroSection onStartDemo={handleStartDemo} />
<ProductDemoSection
simulation={simulation}
onNext={handleNext}
onReset={handleReset}
demoStepTitle={currentDemoStep.title}
/>
<ScenarioCards
scenarios={SCENARIOS}
activeScenarioId={activeScenarioId}
onScenarioChange={handleScenarioChange}
/>
<CriteriaCards />
<EngineeringDetails simulation={simulation} />
</main>
<footer className="site-footer">
<p>OZON Tech Hackathon 2026. Sorter Simulation.</p>
</footer>
</div>
);
}