feat(frontend): implement main dashboard page (#48)
Implement the main dashboard / projects list page for Syndarix as the landing page after login. The implementation includes: Dashboard Components: - QuickStats: Overview cards showing active projects, agents, issues, approvals - ProjectsSection: Grid/list view with filtering and sorting controls - ProjectCardGrid: Rich project cards for grid view - ProjectRowList: Compact rows for list view - ActivityFeed: Real-time activity sidebar with connection status - PerformanceCard: Performance metrics display - EmptyState: Call-to-action for new users - ProjectStatusBadge: Status indicator with icons - ComplexityIndicator: Visual complexity dots - ProgressBar: Accessible progress bar component Features: - Projects grid/list view with view mode toggle - Filter by status (all, active, paused, completed, archived) - Sort by recent, name, progress, or issues - Quick stats overview with counts - Real-time activity feed sidebar with live/reconnecting status - Performance metrics card - Create project button linking to wizard - Responsive layout for mobile/desktop - Loading skeleton states - Empty state for new users API Integration: - useProjects hook for fetching projects (mock data until backend ready) - useDashboardStats hook for statistics - TanStack Query for caching and data fetching Testing: - 37 unit tests covering all dashboard components - E2E test suite for dashboard functionality - Accessibility tests (keyboard nav, aria attributes, heading hierarchy) Technical: - TypeScript strict mode compliance - ESLint passing - WCAG AA accessibility compliance - Mobile-first responsive design - Dark mode support via semantic tokens - Follows design system guidelines
This commit is contained in:
164
frontend/src/components/projects/wizard/useWizardState.ts
Normal file
164
frontend/src/components/projects/wizard/useWizardState.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Custom hook for managing wizard state
|
||||
*
|
||||
* Handles step navigation logic including script mode shortcuts.
|
||||
*/
|
||||
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
import type { WizardState, WizardStep } from './types';
|
||||
import { initialWizardState } from './types';
|
||||
import { WIZARD_STEPS } from './constants';
|
||||
|
||||
interface UseWizardStateReturn {
|
||||
state: WizardState;
|
||||
updateState: (updates: Partial<WizardState>) => void;
|
||||
resetState: () => void;
|
||||
isScriptMode: boolean;
|
||||
canProceed: boolean;
|
||||
goNext: () => void;
|
||||
goBack: () => void;
|
||||
getProjectData: () => ProjectCreateData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Data structure for project creation API call
|
||||
*/
|
||||
export interface ProjectCreateData {
|
||||
name: string;
|
||||
slug: string;
|
||||
description: string | undefined;
|
||||
autonomy_level: 'full_control' | 'milestone' | 'autonomous';
|
||||
settings: {
|
||||
complexity: string;
|
||||
client_mode: string;
|
||||
repo_url?: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a URL-safe slug from a project name
|
||||
*/
|
||||
function generateSlug(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\w\s-]/g, '') // Remove special characters
|
||||
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||
.replace(/-+/g, '-') // Replace multiple hyphens with single
|
||||
.replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
|
||||
}
|
||||
|
||||
export function useWizardState(): UseWizardStateReturn {
|
||||
const [state, setState] = useState<WizardState>(initialWizardState);
|
||||
|
||||
const isScriptMode = state.complexity === 'script';
|
||||
|
||||
const updateState = useCallback((updates: Partial<WizardState>) => {
|
||||
setState((prev) => ({ ...prev, ...updates }));
|
||||
}, []);
|
||||
|
||||
const resetState = useCallback(() => {
|
||||
setState(initialWizardState);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Check if user can proceed to next step
|
||||
*/
|
||||
const canProceed = (() => {
|
||||
switch (state.step) {
|
||||
case WIZARD_STEPS.BASIC_INFO:
|
||||
return state.projectName.trim().length >= 3;
|
||||
case WIZARD_STEPS.COMPLEXITY:
|
||||
return state.complexity !== null;
|
||||
case WIZARD_STEPS.CLIENT_MODE:
|
||||
return isScriptMode || state.clientMode !== null;
|
||||
case WIZARD_STEPS.AUTONOMY:
|
||||
return isScriptMode || state.autonomyLevel !== null;
|
||||
case WIZARD_STEPS.AGENT_CHAT:
|
||||
return true; // Agent chat is preview only
|
||||
case WIZARD_STEPS.REVIEW:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
/**
|
||||
* Navigate to next step, handling script mode skip logic
|
||||
*/
|
||||
const goNext = useCallback(() => {
|
||||
if (!canProceed) return;
|
||||
|
||||
setState((prev) => {
|
||||
let nextStep = (prev.step + 1) as WizardStep;
|
||||
const currentIsScriptMode = prev.complexity === 'script';
|
||||
|
||||
// For scripts, skip from step 2 directly to step 5 (agent chat)
|
||||
if (currentIsScriptMode && prev.step === WIZARD_STEPS.COMPLEXITY) {
|
||||
return {
|
||||
...prev,
|
||||
step: WIZARD_STEPS.AGENT_CHAT as WizardStep,
|
||||
clientMode: 'auto',
|
||||
autonomyLevel: 'autonomous',
|
||||
};
|
||||
}
|
||||
|
||||
// Don't go past review step
|
||||
if (nextStep > WIZARD_STEPS.REVIEW) {
|
||||
nextStep = WIZARD_STEPS.REVIEW as WizardStep;
|
||||
}
|
||||
|
||||
return { ...prev, step: nextStep };
|
||||
});
|
||||
}, [canProceed]);
|
||||
|
||||
/**
|
||||
* Navigate to previous step, handling script mode skip logic
|
||||
*/
|
||||
const goBack = useCallback(() => {
|
||||
setState((prev) => {
|
||||
if (prev.step <= 1) return prev;
|
||||
|
||||
let prevStep = (prev.step - 1) as WizardStep;
|
||||
const currentIsScriptMode = prev.complexity === 'script';
|
||||
|
||||
// For scripts, go from step 5 back to step 2
|
||||
if (currentIsScriptMode && prev.step === WIZARD_STEPS.AGENT_CHAT) {
|
||||
prevStep = WIZARD_STEPS.COMPLEXITY as WizardStep;
|
||||
}
|
||||
|
||||
return { ...prev, step: prevStep };
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Get data formatted for the project creation API
|
||||
*/
|
||||
const getProjectData = useCallback((): ProjectCreateData => {
|
||||
const slug = generateSlug(state.projectName);
|
||||
|
||||
return {
|
||||
name: state.projectName.trim(),
|
||||
slug,
|
||||
description: state.description.trim() || undefined,
|
||||
autonomy_level: state.autonomyLevel || 'milestone',
|
||||
settings: {
|
||||
complexity: state.complexity || 'medium',
|
||||
client_mode: state.clientMode || 'auto',
|
||||
...(state.repoUrl && { repo_url: state.repoUrl }),
|
||||
},
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
return {
|
||||
state,
|
||||
updateState,
|
||||
resetState,
|
||||
isScriptMode,
|
||||
canProceed,
|
||||
goNext,
|
||||
goBack,
|
||||
getProjectData,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user