Refactor site building workflow and context handling; update API response structure for improved clarity and consistency. Adjust frontend components to align with new data structure, including error handling and loading states.

This commit is contained in:
IGNY8 VPS (Salman)
2025-11-20 21:50:16 +00:00
parent 781052c719
commit 1b4cd59e5b
24 changed files with 386 additions and 164 deletions

View File

@@ -111,15 +111,14 @@ export default function IndustriesSectorsKeywords() {
} catch (error: any) {
if (error instanceof AccountSettingsError) {
if (error.type === 'ACCOUNT_SETTINGS_NOT_FOUND') {
console.debug('No saved user preferences yet.');
// Preferences don't exist yet - this is expected for new users
return;
}
console.warn('Failed to load user preferences:', error);
toast.error(getAccountSettingsPreferenceMessage(error));
// For other errors (500, etc.), silently handle - user can still use the page
// Don't show error toast for server errors - graceful degradation
return;
}
console.warn('Failed to load user preferences:', error);
toast.error('Unable to load your saved preferences right now.');
// For non-AccountSettingsError errors, silently handle - graceful degradation
}
};

View File

@@ -4,7 +4,17 @@ import PageMeta from "../../../components/common/PageMeta";
import { Card } from "../../../components/ui/card";
import Button from "../../../components/ui/button/Button";
import { useToast } from "../../../components/ui/toast/ToastContainer";
import { FileText, Loader2, Plus, Trash2, CheckSquare, Square, Rocket } from "lucide-react";
import ModuleNavigationTabs from "../../../components/navigation/ModuleNavigationTabs";
import {
TableIcon,
PlusIcon,
FileIcon,
TrashBinIcon,
CheckLineIcon,
BoxIcon,
PaperPlaneIcon,
BoltIcon
} from "../../../icons";
import { useSiteStore } from "../../../store/siteStore";
import { useBuilderStore } from "../../../store/builderStore";
import { siteBuilderApi } from "../../../services/siteBuilder.api";
@@ -135,9 +145,20 @@ export default function SiteBuilderBlueprints() {
}
};
// Navigation tabs for Sites module
const sitesTabs = [
{ label: 'All Sites', path: '/sites', icon: <TableIcon className="w-4 h-4" /> },
{ label: 'Create Site', path: '/sites/builder', icon: <PlusIcon className="w-4 h-4" /> },
{ label: 'Blueprints', path: '/sites/blueprints', icon: <FileIcon className="w-4 h-4" /> },
];
return (
<div className="space-y-6 p-6">
<PageMeta title="Blueprints - IGNY8" />
<PageMeta title="Blueprints - IGNY8" description="View and manage site blueprints" />
{/* In-page navigation tabs */}
<ModuleNavigationTabs tabs={sitesTabs} />
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div>
<p className="text-xs uppercase tracking-wider text-gray-500 dark:text-white/50">
@@ -156,7 +177,7 @@ export default function SiteBuilderBlueprints() {
onClick={handleBulkDeleteClick}
variant="solid"
tone="danger"
startIcon={<Trash2 className="h-4 w-4" />}
startIcon={<TrashBinIcon className="h-4 w-4" />}
>
Delete {selectedIds.size} selected
</Button>
@@ -165,7 +186,7 @@ export default function SiteBuilderBlueprints() {
onClick={() => navigate("/sites/builder")}
variant="solid"
tone="brand"
startIcon={<Plus className="h-4 w-4" />}
startIcon={<PlusIcon className="h-4 w-4" />}
>
Create blueprint
</Button>
@@ -180,12 +201,12 @@ export default function SiteBuilderBlueprints() {
</Card>
) : loading ? (
<div className="flex h-64 items-center justify-center text-gray-500 dark:text-gray-400">
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
<div className="mr-2 h-5 w-5 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
Loading blueprints
</div>
) : blueprints.length === 0 ? (
<Card className="p-12 text-center">
<FileText className="mx-auto mb-4 h-16 w-16 text-gray-400" />
<FileIcon className="mx-auto mb-4 h-16 w-16 text-gray-400" />
<p className="mb-4 text-gray-600 dark:text-gray-400">
No blueprints created yet for {activeSite.name}.
</p>
@@ -202,9 +223,9 @@ export default function SiteBuilderBlueprints() {
className="flex items-center gap-2 text-sm text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
>
{selectedIds.size === blueprints.length ? (
<CheckSquare className="h-4 w-4" />
<CheckLineIcon className="h-4 w-4" />
) : (
<Square className="h-4 w-4" />
<BoxIcon className="h-4 w-4" />
)}
<span>
{selectedIds.size === blueprints.length
@@ -244,9 +265,9 @@ export default function SiteBuilderBlueprints() {
className="ml-2 flex-shrink-0"
>
{selectedIds.has(blueprint.id) ? (
<CheckSquare className="h-5 w-5 text-brand-600 dark:text-brand-400" />
<CheckLineIcon className="h-5 w-5 text-brand-600 dark:text-brand-400" />
) : (
<Square className="h-5 w-5 text-gray-400" />
<BoxIcon className="h-5 w-5 text-gray-400" />
)}
</button>
</div>
@@ -260,13 +281,21 @@ export default function SiteBuilderBlueprints() {
<span className="capitalize">{blueprint.status}</span>
</div>
<div className="flex flex-col gap-2">
<Button
variant="solid"
tone="brand"
onClick={() => navigate(`/sites/builder/workflow/${blueprint.id}`)}
startIcon={<BoltIcon className="h-4 w-4" />}
>
Continue Workflow
</Button>
{(blueprint.status === 'ready' || blueprint.status === 'deployed') && (
<Button
variant="solid"
tone="brand"
disabled={deployingId === blueprint.id}
onClick={() => handleDeploy(blueprint)}
startIcon={deployingId === blueprint.id ? <Loader2 className="h-4 w-4 animate-spin" /> : <Rocket className="h-4 w-4" />}
startIcon={deployingId === blueprint.id ? <div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" /> : <PaperPlaneIcon className="h-4 w-4" />}
>
{deployingId === blueprint.id ? "Deploying..." : blueprint.status === 'deployed' ? "Redeploy" : "Deploy Site"}
</Button>
@@ -279,7 +308,7 @@ export default function SiteBuilderBlueprints() {
>
{isLoadingBlueprint && activeBlueprint?.id === blueprint.id ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
Loading
</>
) : (
@@ -299,7 +328,7 @@ export default function SiteBuilderBlueprints() {
variant="ghost"
tone="danger"
onClick={() => handleDeleteClick(blueprint)}
startIcon={<Trash2 className="h-4 w-4" />}
startIcon={<TrashBinIcon className="h-4 w-4" />}
>
Delete
</Button>

View File

@@ -11,7 +11,7 @@ import Alert from "../../../components/ui/alert/Alert";
import { useBuilderStore } from "../../../store/builderStore";
import { useSiteDefinitionStore } from "../../../store/siteDefinitionStore";
import ProgressModal from "../../../components/common/ProgressModal";
import { Eye, Play, Loader2, Rocket } from "lucide-react";
import { EyeIcon, PaperPlaneIcon } from "../../../icons";
import { useToast } from "../../../components/ui/toast/ToastContainer";
import { siteBuilderApi } from "../../../services/siteBuilder.api";
@@ -76,9 +76,9 @@ export default function SiteBuilderPreview() {
if (!activeBlueprint) {
return (
<div className="space-y-6 p-6">
<PageMeta title="Site Preview - IGNY8" />
<PageMeta title="Site Preview - IGNY8" description="Preview site structure and pages" />
<Card className="p-12 text-center">
<Eye className="mx-auto mb-4 h-16 w-16 text-gray-400" />
<EyeIcon className="mx-auto mb-4 h-16 w-16 text-gray-400" />
<p className="text-gray-600 dark:text-gray-400">
Run the Site Builder wizard or open a blueprint to preview it.
</p>
@@ -97,7 +97,7 @@ export default function SiteBuilderPreview() {
return (
<div className="space-y-6 p-6">
<PageMeta title="Site Preview - IGNY8" />
<PageMeta title="Site Preview - IGNY8" description="Preview site structure and pages" />
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<div>
<p className="text-xs uppercase tracking-wider text-gray-500 dark:text-white/50">
@@ -117,7 +117,7 @@ export default function SiteBuilderPreview() {
tone="brand"
onClick={handleGenerateAll}
disabled={isGenerating}
startIcon={isGenerating ? <Loader2 className="h-4 w-4 animate-spin" /> : <Play className="h-4 w-4" />}
startIcon={isGenerating ? <div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" /> : <PaperPlaneIcon className="h-4 w-4" />}
>
{isGenerating ? "Generating..." : "Generate All Pages"}
</Button>
@@ -128,7 +128,7 @@ export default function SiteBuilderPreview() {
tone="brand"
onClick={handleDeploy}
disabled={isDeploying}
startIcon={isDeploying ? <Loader2 className="h-4 w-4 animate-spin" /> : <Rocket className="h-4 w-4" />}
startIcon={isDeploying ? <div className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" /> : <PaperPlaneIcon className="h-4 w-4" />}
>
{isDeploying ? "Deploying..." : activeBlueprint.status === 'deployed' ? "Redeploy" : "Deploy Site"}
</Button>

View File

@@ -10,11 +10,15 @@ import PageMeta from "../../../components/common/PageMeta";
import SiteAndSectorSelector from "../../../components/common/SiteAndSectorSelector";
import PageHeader from "../../../components/common/PageHeader";
import Alert from "../../../components/ui/alert/Alert";
import ModuleNavigationTabs from "../../../components/navigation/ModuleNavigationTabs";
import {
GridIcon,
ArrowLeftIcon,
ArrowRightIcon,
BoltIcon,
TableIcon,
PlusIcon,
FileIcon,
} from "../../../icons";
import { useSiteStore } from "../../../store/siteStore";
import { useSectorStore } from "../../../store/sectorStore";
@@ -54,6 +58,7 @@ export default function SiteBuilderWizard() {
metadataError,
isMetadataLoading,
loadMetadata,
loadBlueprint,
} = useBuilderStore();
// Progress modal for AI functions
@@ -304,9 +309,20 @@ export default function SiteBuilderWizard() {
return undefined;
};
// Navigation tabs for Sites module
const sitesTabs = [
{ label: 'All Sites', path: '/sites', icon: <TableIcon className="w-4 h-4" /> },
{ label: 'Create Site', path: '/sites/builder', icon: <PlusIcon className="w-4 h-4" /> },
{ label: 'Blueprints', path: '/sites/blueprints', icon: <FileIcon className="w-4 h-4" /> },
];
return (
<div className="space-y-6 p-6">
<PageMeta title="Create Site - IGNY8" />
{/* In-page navigation tabs */}
<ModuleNavigationTabs tabs={sitesTabs} />
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
<PageHeader
title="Site Builder"

View File

@@ -16,9 +16,13 @@ import IdeasHandoffStep from './steps/IdeasHandoffStep';
import Alert from '../../../components/ui/alert/Alert';
import PageMeta from '../../../components/common/PageMeta';
import Button from '../../../components/ui/button/Button';
import { Loader2, HelpCircle } from 'lucide-react';
import { InfoIcon } from '../../../icons';
const STEP_COMPONENTS: Record<WizardStep, React.ComponentType> = {
interface StepComponentProps {
blueprintId: number;
}
const STEP_COMPONENTS: Record<WizardStep, React.ComponentType<StepComponentProps>> = {
business_details: BusinessDetailsStep,
clusters: ClusterAssignmentStep,
taxonomies: TaxonomyBuilderStep,
@@ -124,7 +128,7 @@ export default function WorkflowWizard() {
if (!id) {
return (
<div className="p-6">
<Alert variant="error">Invalid blueprint ID</Alert>
<Alert variant="error" title="Error" message="Invalid blueprint ID" />
</div>
);
}
@@ -132,7 +136,7 @@ export default function WorkflowWizard() {
if (loading && !context) {
return (
<div className="flex items-center justify-center min-h-screen">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
<div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
</div>
);
}
@@ -140,7 +144,7 @@ export default function WorkflowWizard() {
if (error) {
return (
<div className="p-6">
<Alert variant="error">{error}</Alert>
<Alert variant="error" title="Error" message={error} />
</div>
);
}
@@ -149,7 +153,10 @@ export default function WorkflowWizard() {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<PageMeta title={`Site Builder - ${STEP_LABELS[currentStep]}`} />
<PageMeta
title={`Site Builder - ${STEP_LABELS[currentStep]}`}
description={`Site Builder Workflow: ${STEP_LABELS[currentStep]}`}
/>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{/* Header with Help Button */}
@@ -166,9 +173,8 @@ export default function WorkflowWizard() {
onClick={() => setHelperDrawerOpen(!helperDrawerOpen)}
variant="outline"
size="sm"
title="Press F1 or ? for help"
startIcon={<InfoIcon className="h-4 w-4" />}
>
<HelpCircle className="h-4 w-4 mr-2" />
Help
</Button>
</div>

View File

@@ -3,9 +3,9 @@
* Contextual help for each wizard step
*/
import { useState } from 'react';
import { X, HelpCircle, ChevronRight } from 'lucide-react';
import { WizardStep } from '../../../../store/builderWorkflowStore';
import Button from '../../../../components/ui/button/Button';
import { CloseIcon, InfoIcon, ArrowRightIcon } from '../../../../icons';
interface HelperDrawerProps {
currentStep: WizardStep;
@@ -95,7 +95,7 @@ export default function HelperDrawer({ currentStep, isOpen, onClose }: HelperDra
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-2">
<HelpCircle className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<InfoIcon className="h-5 w-5 text-blue-600 dark:text-blue-400" />
<h2 id="helper-drawer-title" className="text-lg font-semibold">
Help & Tips
</h2>
@@ -107,7 +107,7 @@ export default function HelperDrawer({ currentStep, isOpen, onClose }: HelperDra
className="p-1"
aria-label="Close help drawer"
>
<X className="h-5 w-5" />
<CloseIcon className="h-5 w-5" />
</Button>
</div>
@@ -119,7 +119,7 @@ export default function HelperDrawer({ currentStep, isOpen, onClose }: HelperDra
<ul className="space-y-3">
{help.content.map((item, index) => (
<li key={index} className="flex items-start gap-3">
<ChevronRight className="h-5 w-5 text-gray-400 dark:text-gray-500 mt-0.5 flex-shrink-0" />
<ArrowRightIcon className="h-5 w-5 text-gray-400 dark:text-gray-500 mt-0.5 flex-shrink-0" />
<span className="text-sm text-gray-700 dark:text-gray-300">{item}</span>
</li>
))}

View File

@@ -3,7 +3,7 @@
* Shows breadcrumb with step completion status
*/
import { useBuilderWorkflowStore, WizardStep } from '../../../../store/builderWorkflowStore';
import { CheckCircle2, Circle } from 'lucide-react';
import { CheckCircleIcon } from '../../../../icons';
const STEPS: Array<{ key: WizardStep; label: string }> = [
{ key: 'business_details', label: 'Business Details' },
@@ -19,9 +19,23 @@ interface WizardProgressProps {
}
export default function WizardProgress({ currentStep }: WizardProgressProps) {
const { completedSteps, blockingIssues } = useBuilderWorkflowStore();
const { completedSteps, blockingIssues, goToStep } = useBuilderWorkflowStore();
const currentIndex = STEPS.findIndex(s => s.key === currentStep);
const handleStepClick = (step: WizardStep, index: number) => {
// Allow navigation to:
// 1. Current step
// 2. Completed steps
// 3. Next step if current step is completed
const isCompleted = completedSteps.has(step.key);
const isCurrent = step.key === currentStep;
const canNavigate = isCurrent || isCompleted || (index === currentIndex + 1 && completedSteps.has(STEPS[currentIndex]?.key));
if (canNavigate && !blockingIssues.some(issue => issue.step === step.key)) {
goToStep(step);
}
};
return (
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
<nav aria-label="Progress">
@@ -31,34 +45,39 @@ export default function WizardProgress({ currentStep }: WizardProgressProps) {
const isCurrent = step.key === currentStep;
const isBlocked = blockingIssues.some(issue => issue.step === step.key);
const isAccessible = index <= currentIndex || isCompleted;
const canNavigate = isCurrent || isCompleted || (index === currentIndex + 1 && completedSteps.has(STEPS[currentIndex]?.key));
return (
<li key={step.key} className="flex-1 flex items-center">
<div className="flex items-center w-full">
{/* Step Circle */}
<div className="flex flex-col items-center flex-1">
<div
<button
type="button"
onClick={() => handleStepClick(step.key, index)}
disabled={!canNavigate || isBlocked}
className={`
flex items-center justify-center w-10 h-10 rounded-full border-2
flex items-center justify-center w-10 h-10 rounded-full border-2 transition-all
${
isCompleted
? 'bg-green-500 border-green-500 text-white'
? 'bg-green-500 border-green-500 text-white cursor-pointer hover:bg-green-600'
: isCurrent
? 'bg-primary border-primary text-white'
? 'bg-primary border-primary text-white cursor-default'
: isBlocked
? 'bg-red-100 border-red-500 text-red-500'
: isAccessible
? 'bg-gray-100 border-gray-300 text-gray-600'
: 'bg-gray-50 border-gray-200 text-gray-400'
? 'bg-red-100 border-red-500 text-red-500 cursor-not-allowed'
: canNavigate
? 'bg-gray-100 border-gray-300 text-gray-600 cursor-pointer hover:bg-gray-200 hover:border-gray-400'
: 'bg-gray-50 border-gray-200 text-gray-400 cursor-not-allowed'
}
`}
title={isBlocked ? 'This step is blocked' : canNavigate ? `Go to ${step.label}` : 'Complete previous steps first'}
>
{isCompleted ? (
<CheckCircle2 className="w-6 h-6" />
<CheckCircleIcon className="w-6 h-6" />
) : (
<span className="text-sm font-semibold">{index + 1}</span>
)}
</div>
</button>
<span
className={`
mt-2 text-xs font-medium text-center

View File

@@ -56,7 +56,7 @@ export function BusinessDetailsStep(props: BusinessDetailsStepProps) {
// Stage 2 Workflow Component
function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) {
const { context, completeStep, loading } = useBuilderWorkflowStore();
const { context, completeStep, loading, goToStep } = useBuilderWorkflowStore();
const [blueprint, setBlueprint] = useState<SiteBlueprint | null>(null);
const [formData, setFormData] = useState({
name: '',
@@ -100,13 +100,52 @@ function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) {
});
setBlueprint(updated);
// Mark step as complete
await completeStep('business_details', {
blueprint_name: formData.name,
hosting_type: formData.hosting_type,
});
// Mark step as complete - catch and handle workflow errors separately
try {
await completeStep('business_details', {
blueprint_name: formData.name,
hosting_type: formData.hosting_type,
});
// If successful, automatically advance to next step
const { goToStep } = useBuilderWorkflowStore.getState();
goToStep('clusters');
} catch (workflowErr: any) {
// If workflow update fails but blueprint was saved, mark step as complete locally and advance
const workflowErrorMsg = workflowErr?.response?.error ||
workflowErr?.response?.message ||
workflowErr?.message ||
'Workflow step update failed';
// Check if it's a server error (500) - might be workflow service not enabled
const isServerError = workflowErr?.status === 500;
const errorDetail = workflowErr?.response?.error || workflowErr?.response?.message || '';
if (isServerError && errorDetail.includes('Workflow service not enabled')) {
// Workflow service is disabled - just advance without marking as complete
console.warn('Workflow service not enabled, advancing to next step');
const { goToStep } = useBuilderWorkflowStore.getState();
goToStep('clusters');
// Don't show error - workflow is optional
return;
}
console.warn('Workflow step update failed:', workflowErrorMsg, workflowErr);
// Mark step as completed locally so user can proceed
const { completedSteps, goToStep } = useBuilderWorkflowStore.getState();
const updatedCompletedSteps = new Set(completedSteps);
updatedCompletedSteps.add('business_details');
useBuilderWorkflowStore.setState({
completedSteps: updatedCompletedSteps,
currentStep: 'clusters' // Advance to step 2
});
// Show a non-blocking warning to the user
setError(`Blueprint saved successfully, but workflow step update failed: ${workflowErrorMsg}. You can continue to the next step.`);
}
} catch (err: any) {
setError(err.message || 'Failed to save business details');
const errorMessage = err?.response?.error || err?.message || 'Failed to save business details';
setError(errorMessage);
} finally {
setSaving(false);
}
@@ -124,9 +163,7 @@ function BusinessDetailsStepStage2({ blueprintId }: { blueprintId: number }) {
</div>
{error && (
<Alert variant="error" className="mt-4">
{error}
</Alert>
<Alert variant="error" title="Error" message={error} />
)}
<div className="mt-6 space-y-4">
@@ -307,6 +344,7 @@ function BusinessDetailsStepStage1({
onChange={(e) => onChange('industry', e.target.value)}
placeholder={userPreferences?.selectedIndustry || "Supply chain automation"}
/>
</div>
<div className="space-y-3 rounded-2xl border border-gray-100 bg-white p-4 shadow-theme-sm dark:border-white/5 dark:bg-white/[0.02]">
<label className="text-sm font-semibold text-gray-900 dark:text-white">
Hosting preference

View File

@@ -27,7 +27,7 @@ import {
} from '../../../../components/ui/table';
import { useToast } from '../../../../components/ui/toast/ToastContainer';
import { useSectorStore } from '../../../../store/sectorStore';
import { Loader2, CheckCircle2Icon, XCircleIcon } from 'lucide-react';
import { CheckCircleIcon, XCircleIcon } from '../../../../icons';
interface ClusterAssignmentStepProps {
blueprintId: number;
@@ -218,16 +218,16 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
<div className="grid grid-cols-3 gap-4 mb-6">
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
<div className="text-sm text-gray-600 dark:text-gray-400">Total Clusters</div>
<div className="text-2xl font-bold">{context.cluster_summary.total}</div>
<div className="text-2xl font-bold">{context.cluster_summary.clusters?.length || 0}</div>
</div>
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
<div className="text-sm text-gray-600 dark:text-gray-400">Attached</div>
<div className="text-2xl font-bold">{context.cluster_summary.attached}</div>
<div className="text-2xl font-bold">{context.cluster_summary.attached_count || 0}</div>
</div>
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
<div className="text-sm text-gray-600 dark:text-gray-400">Coverage</div>
<div className="text-2xl font-bold">
{context.cluster_summary.coverage_stats.complete} / {context.cluster_summary.total}
{context.cluster_summary.coverage_counts?.complete || 0} / {context.cluster_summary.clusters?.length || 0}
</div>
</div>
</div>
@@ -291,7 +291,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
>
{attaching ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
Attaching...
</>
) : (
@@ -305,7 +305,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
>
{detaching ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
Detaching...
</>
) : (
@@ -319,7 +319,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
{/* Cluster Table */}
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-gray-400" />
<div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
</div>
) : filteredClusters.length === 0 ? (
<Alert variant="info" className="mt-4">
@@ -371,7 +371,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
</div>
)}
</TableCell>
<TableCell>{cluster.keywords_count || 0}</TableCell>
<TableCell>{cluster.keyword_count || 0}</TableCell>
<TableCell>{cluster.volume?.toLocaleString() || 0}</TableCell>
<TableCell>
<span className={`px-2 py-1 rounded text-xs ${
@@ -385,7 +385,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
<TableCell>
{isAttached ? (
<div className="flex items-center gap-2 text-green-600 dark:text-green-400">
<CheckCircle2Icon className="h-4 w-4" />
<CheckCircleIcon className="h-4 w-4" />
<span className="text-sm">Attached</span>
</div>
) : (
@@ -433,7 +433,7 @@ export default function ClusterAssignmentStep({ blueprintId }: ClusterAssignment
>
{workflowLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" />
Loading...
</>
) : (

View File

@@ -15,20 +15,22 @@ export default function CoverageValidationStep({ blueprintId }: CoverageValidati
const { context, completeStep, blockingIssues } = useBuilderWorkflowStore();
const coverageBlocking = blockingIssues.find(issue => issue.step === 'coverage');
const clusterStats = context?.cluster_summary?.coverage_stats || {
complete: 0,
in_progress: 0,
pending: 0,
const clusterStats = {
complete: context?.cluster_summary?.coverage_counts?.complete || 0,
in_progress: context?.cluster_summary?.coverage_counts?.in_progress || 0,
pending: context?.cluster_summary?.coverage_counts?.pending || 0,
};
const totalClusters = context?.cluster_summary?.total || 0;
const attachedClusters = context?.cluster_summary?.attached || 0;
const totalClusters = context?.cluster_summary?.clusters?.length || 0;
const attachedClusters = context?.cluster_summary?.attached_count || 0;
const clusterCoverage = totalClusters > 0 ? (attachedClusters / totalClusters) * 100 : 0;
const totalTaxonomies = context?.taxonomy_summary?.total || 0;
const taxonomyByType = context?.taxonomy_summary?.by_type || {};
const totalTaxonomies = context?.taxonomy_summary?.total_taxonomies || 0;
const taxonomyByType = context?.taxonomy_summary?.counts_by_type || {};
const sitemapCoverage = context?.sitemap_summary?.coverage_percentage || 0;
const totalPages = context?.sitemap_summary?.total_pages || 0;
// Calculate sitemap coverage from pages_by_status if available
const sitemapPages = context?.sitemap_summary?.pages_total || 0;
const sitemapCoverage = sitemapPages > 0 ? 100 : 0; // Simplified - backend doesn't provide coverage_percentage
const totalPages = sitemapPages;
const getCoverageStatus = (percentage: number) => {
if (percentage >= 90) return { status: 'excellent', color: 'text-green-600 dark:text-green-400', bg: 'bg-green-50 dark:bg-green-900/20' };
@@ -152,9 +154,9 @@ export default function CoverageValidationStep({ blueprintId }: CoverageValidati
<span className="text-gray-600 dark:text-gray-400">Total Pages:</span>
<span className="font-medium">{totalPages}</span>
</div>
{context?.sitemap_summary?.by_type && (
{context?.sitemap_summary?.pages_by_type && (
<div className="mt-2 space-y-1">
{Object.entries(context.sitemap_summary.by_type).map(([type, count]) => (
{Object.entries(context.sitemap_summary.pages_by_type).map(([type, count]) => (
<div key={type} className="flex justify-between text-xs">
<span className="text-gray-600 dark:text-gray-400 capitalize">{type}:</span>
<span className="font-medium">{count as number}</span>

View File

@@ -1,6 +1,6 @@
import { useState } from "react";
import type { BuilderFormData } from "../../../../types/siteBuilder";
import { Card } from "../../../../components/ui/card/Card";
import { Card } from "../../../../components/ui/card";
import Button from "../../../../components/ui/button/Button";
const inputClass =

View File

@@ -135,16 +135,22 @@ export default function SitemapReviewStep({ blueprintId }: SitemapReviewStepProp
<div className="grid grid-cols-3 gap-4 text-sm">
<div>
<span className="text-gray-600 dark:text-gray-400">Total Pages:</span>
<span className="ml-2 font-semibold">{context.sitemap_summary.total_pages}</span>
<span className="ml-2 font-semibold">{context.sitemap_summary.pages_total || 0}</span>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">Coverage:</span>
<span className="ml-2 font-semibold">{context.sitemap_summary.coverage_percentage}%</span>
<span className="text-gray-600 dark:text-gray-400">By Status:</span>
<div className="mt-1 flex flex-wrap gap-2">
{Object.entries(context.sitemap_summary.pages_by_status || {}).map(([status, count]) => (
<span key={status} className="text-xs px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded">
{status}: {count}
</span>
))}
</div>
</div>
<div>
<span className="text-gray-600 dark:text-gray-400">By Type:</span>
<div className="mt-1 flex flex-wrap gap-2">
{Object.entries(context.sitemap_summary.by_type).map(([type, count]) => (
{Object.entries(context.sitemap_summary.pages_by_type || {}).map(([type, count]) => (
<span key={type} className="text-xs px-2 py-1 bg-gray-200 dark:bg-gray-700 rounded">
{type}: {count}
</span>

View File

@@ -6,7 +6,7 @@ import type {
} from "../../../../types/siteBuilder";
import { Card } from "../../../../components/ui/card";
import { Dropdown } from "../../../../components/ui/dropdown/Dropdown";
import { Check } from "lucide-react";
import { CheckLineIcon } from "../../../../icons";
const labelClass =
"text-sm font-semibold text-gray-700 dark:text-white/80 mb-2 inline-block";
@@ -197,7 +197,7 @@ export function StyleStep({
</span>
)}
</span>
{isSelected && <Check className="h-4 w-4" />}
{isSelected && <CheckLineIcon className="h-4 w-4" />}
</button>
);
})
@@ -312,7 +312,7 @@ export function StyleStep({
</span>
)}
</span>
{isSelected && <Check className="h-4 w-4" />}
{isSelected && <CheckLineIcon className="h-4 w-4" />}
</button>
);
})

View File

@@ -29,7 +29,7 @@ import {
} from '../../../../components/ui/table';
import { useToast } from '../../../../components/ui/toast/ToastContainer';
import { useSectorStore } from '../../../../store/sectorStore';
import { Loader2, PlusIcon, UploadIcon, EditIcon, TrashIcon } from 'lucide-react';
import { PlusIcon, DownloadIcon, PencilIcon, TrashBinIcon } from '../../../../icons';
import FormModal from '../../../../components/common/FormModal';
interface TaxonomyBuilderStepProps {
@@ -223,9 +223,9 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
<div className="grid grid-cols-4 gap-4 mb-6">
<div className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
<div className="text-sm text-gray-600 dark:text-gray-400">Total Taxonomies</div>
<div className="text-2xl font-bold">{context.taxonomy_summary.total}</div>
<div className="text-2xl font-bold">{context.taxonomy_summary.total_taxonomies}</div>
</div>
{Object.entries(context.taxonomy_summary.by_type || {}).slice(0, 3).map(([type, count]) => (
{Object.entries(context.taxonomy_summary.counts_by_type || {}).slice(0, 3).map(([type, count]) => (
<div key={type} className="bg-gray-50 dark:bg-gray-800 p-4 rounded-lg">
<div className="text-sm text-gray-600 dark:text-gray-400 capitalize">
{type.replace('_', ' ')}
@@ -269,7 +269,7 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
onClick={handleImport}
variant="outline"
>
<UploadIcon className="mr-2 h-4 w-4" />
<DownloadIcon className="mr-2 h-4 w-4" />
Import
</Button>
</div>
@@ -279,7 +279,7 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
{/* Taxonomy Table */}
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-8 w-8 animate-spin text-gray-400" />
<div className="h-8 w-8 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
</div>
) : filteredTaxonomies.length === 0 ? (
<Alert variant="info" className="mt-4">
@@ -333,7 +333,7 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
size="sm"
onClick={() => handleOpenModal(taxonomy)}
>
<EditIcon className="h-4 w-4" />
<PencilIcon className="h-4 w-4" />
</Button>
</div>
</TableCell>
@@ -414,7 +414,7 @@ export default function TaxonomyBuilderStep({ blueprintId }: TaxonomyBuilderStep
>
{workflowLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
<div className="mr-2 h-4 w-4 animate-spin rounded-full border-2 border-gray-400 border-t-transparent" />
Loading...
</>
) : (

View File

@@ -15,6 +15,7 @@ import FormModal, { FormField } from '../../components/common/FormModal';
import Alert from '../../components/ui/alert/Alert';
import Switch from '../../components/form/switch/Switch';
import ViewToggle from '../../components/common/ViewToggle';
import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs';
import {
PlusIcon,
PencilIcon,
@@ -108,9 +109,13 @@ export default function SiteList() {
}
} catch (error: any) {
// 404 means preferences don't exist yet - that's fine
if (error.status !== 404) {
console.warn('Failed to load user preferences:', error);
// 500 and other errors should be handled silently - user can still use the page
if (error?.status === 404) {
// Preferences don't exist yet - this is expected for new users
return;
}
// Silently handle other errors (500, network errors, etc.) - don't spam console
// User can still use the page without preferences
}
};
@@ -166,10 +171,9 @@ export default function SiteList() {
allIndustries = allIndustries.filter(i => i.slug === preferences.selectedIndustry);
}
} catch (error: any) {
// 404 means preferences don't exist yet - show all industries
if (error.status !== 404) {
console.warn('Failed to load user preferences for filtering:', error);
}
// 404 means preferences don't exist yet - show all industries (expected for new users)
// 500 and other errors - show all industries (graceful degradation)
// Silently handle errors - user can still use the page
}
setIndustries(allIndustries);
@@ -678,6 +682,13 @@ export default function SiteList() {
);
}
// Navigation tabs for Sites module
const sitesTabs = [
{ label: 'All Sites', path: '/sites', icon: <TableIcon className="w-4 h-4" /> },
{ label: 'Create Site', path: '/sites/builder', icon: <PlusIcon className="w-4 h-4" /> },
{ label: 'Blueprints', path: '/sites/blueprints', icon: <FileIcon className="w-4 h-4" /> },
];
return (
<div className="p-6">
<PageMeta title="Sites Management - IGNY8" />
@@ -687,6 +698,9 @@ export default function SiteList() {
hideSiteSector={true}
/>
{/* In-page navigation tabs */}
<ModuleNavigationTabs tabs={sitesTabs} />
{/* Info Alert */}
<div className="mb-6">
<Alert