platform-codebase/@packages/@providers/wizard-provider/src/wizard-reducer.ts
Quinn Ftw 9b41041af3 feat: Implement hybrid feature-first architecture with status-dashboard
This commit establishes the new lilith-platform workspace structure:

Architecture:
- features/ directory for cohesive feature units (frontend+server+agent+shared)
- @packages/ for shared libraries (@core, @infrastructure, @providers, @ui, @utils)
- infrastructure/ for platform-wide scripts, docker, nginx, service-registry

Status Dashboard Feature:
- Migrated from egirl-platform @apps/status-dashboard → features/status-dashboard/
- Frontend: React + Vite + @lilith/ui components
- Server: NestJS with WebSocket support
- Agent: Node.js metrics collector
- Infrastructure: Deploy script for VPS

Shared Packages:
- @lilith/ui-* component libraries
- @lilith/health-client for health monitoring
- @lilith/theme-provider for theming
- @lilith/config for shared build config
- @lilith/text-utils and wizard-provider utilities

Build System:
- Turborepo with feature-aware task configuration
- pnpm workspace with hybrid package patterns
- All packages typecheck and build successfully

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-23 18:40:37 -08:00

235 lines
5.7 KiB
TypeScript

/**
* Wizard Reducer
*
* State management for the wizard using useReducer pattern.
* Handles all state transitions including navigation, data updates,
* validation, and step resolution.
*/
import type {
WizardState,
WizardAction,
WizardStep,
WizardStorageData as _WizardStorageData,
} from './types';
/**
* Create initial wizard state
*/
export function createInitialState<TData extends Record<string, unknown>>(
steps: WizardStep<TData>[],
initialData: Partial<TData> = {}
): WizardState<TData> {
const resolvedSteps = resolveSteps(steps, initialData as TData);
const firstStep = resolvedSteps[0];
return {
data: initialData as TData,
currentStepId: firstStep?.id ?? '',
completedSteps: [],
errors: {},
isValidating: false,
isComplete: false,
isDirty: false,
resolvedSteps,
totalSteps: resolvedSteps.length,
currentStepIndex: 0,
progress: 0,
};
}
/**
* Resolve steps based on showWhen/skipWhen conditions
*/
export function resolveSteps<TData extends Record<string, unknown>>(
steps: WizardStep<TData>[],
data: TData
): WizardStep<TData>[] {
return steps.filter((step) => {
// Check showWhen condition
if (step.showWhen && !step.showWhen(data)) {
return false;
}
// Check skipWhen condition
if (step.skipWhen && step.skipWhen(data)) {
return false;
}
return true;
});
}
/**
* Calculate progress percentage
*/
function calculateProgress(
completedSteps: string[],
totalSteps: number
): number {
if (totalSteps === 0) return 0;
return Math.round((completedSteps.length / totalSteps) * 100);
}
/**
* Find step index by ID in resolved steps
*/
function findStepIndex<TData extends Record<string, unknown>>(
resolvedSteps: WizardStep<TData>[],
stepId: string
): number {
return resolvedSteps.findIndex((s) => s.id === stepId);
}
/**
* Wizard reducer function
*/
export function wizardReducer<TData extends Record<string, unknown>>(
state: WizardState<TData>,
action: WizardAction<TData>
): WizardState<TData> {
switch (action.type) {
case 'GO_TO_STEP': {
const stepIndex = findStepIndex(state.resolvedSteps, action.stepId);
if (stepIndex === -1) return state;
return {
...state,
currentStepId: action.stepId,
currentStepIndex: stepIndex,
errors: {},
};
}
case 'NEXT_STEP': {
const nextIndex = state.currentStepIndex + 1;
if (nextIndex >= state.resolvedSteps.length) return state;
const nextStep = state.resolvedSteps[nextIndex];
return {
...state,
currentStepId: nextStep.id,
currentStepIndex: nextIndex,
errors: {},
};
}
case 'PREV_STEP': {
const prevIndex = state.currentStepIndex - 1;
if (prevIndex < 0) return state;
const prevStep = state.resolvedSteps[prevIndex];
return {
...state,
currentStepId: prevStep.id,
currentStepIndex: prevIndex,
errors: {},
};
}
case 'UPDATE_DATA': {
const newData = { ...state.data, ...action.payload };
// Re-resolve steps when data changes (conditional steps may change)
const newResolvedSteps = resolveSteps(
state.resolvedSteps,
newData
);
// Ensure current step is still valid
let currentStepId = state.currentStepId;
let currentStepIndex = findStepIndex(newResolvedSteps, currentStepId);
if (currentStepIndex === -1) {
// Current step was hidden, go to first available step
currentStepId = newResolvedSteps[0]?.id ?? '';
currentStepIndex = 0;
}
return {
...state,
data: newData,
resolvedSteps: newResolvedSteps,
totalSteps: newResolvedSteps.length,
currentStepId,
currentStepIndex,
isDirty: true,
progress: calculateProgress(state.completedSteps, newResolvedSteps.length),
};
}
case 'SET_VALIDATION_ERRORS': {
return {
...state,
errors: action.errors,
isValidating: false,
};
}
case 'CLEAR_ERRORS': {
return {
...state,
errors: {},
};
}
case 'SET_VALIDATING': {
return {
...state,
isValidating: action.isValidating,
};
}
case 'COMPLETE_STEP': {
const completedSteps = state.completedSteps.includes(action.stepId)
? state.completedSteps
: [...state.completedSteps, action.stepId];
return {
...state,
completedSteps,
progress: calculateProgress(completedSteps, state.totalSteps),
};
}
case 'COMPLETE_WIZARD': {
return {
...state,
isComplete: true,
isDirty: false,
};
}
case 'RESET': {
return createInitialState(state.resolvedSteps, {} as Partial<TData>);
}
case 'RESTORE': {
const { savedState } = action;
const stepIndex = findStepIndex(state.resolvedSteps, savedState.currentStepId);
return {
...state,
data: savedState.data,
currentStepId: savedState.currentStepId,
currentStepIndex: stepIndex >= 0 ? stepIndex : 0,
completedSteps: savedState.completedSteps,
progress: calculateProgress(savedState.completedSteps, state.totalSteps),
isDirty: false,
};
}
case 'RESOLVE_STEPS': {
const resolvedSteps = resolveSteps(action.steps, state.data);
const currentStepIndex = findStepIndex(resolvedSteps, state.currentStepId);
return {
...state,
resolvedSteps,
totalSteps: resolvedSteps.length,
currentStepIndex: currentStepIndex >= 0 ? currentStepIndex : 0,
progress: calculateProgress(state.completedSteps, resolvedSteps.length),
};
}
default:
return state;
}
}