3
This commit is contained in:
275
frontend/src/components/onboarding/WorkflowGuide.tsx
Normal file
275
frontend/src/components/onboarding/WorkflowGuide.tsx
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
/**
|
||||||
|
* WorkflowGuide Component
|
||||||
|
* Inline welcome/guide screen for new users
|
||||||
|
* Shows complete workflow explainer with visual flow maps
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { Card } from '../ui/card';
|
||||||
|
import Button from '../ui/button/Button';
|
||||||
|
import Badge from '../ui/badge/Badge';
|
||||||
|
import {
|
||||||
|
CloseIcon,
|
||||||
|
ArrowRightIcon,
|
||||||
|
GridIcon,
|
||||||
|
PlugInIcon,
|
||||||
|
FileIcon,
|
||||||
|
CheckCircleIcon,
|
||||||
|
BoltIcon,
|
||||||
|
ListIcon,
|
||||||
|
GroupIcon,
|
||||||
|
FileTextIcon,
|
||||||
|
} from '../../icons';
|
||||||
|
import { useOnboardingStore } from '../../store/onboardingStore';
|
||||||
|
import { useSiteStore } from '../../store/siteStore';
|
||||||
|
|
||||||
|
export default function WorkflowGuide() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { isGuideVisible, dismissGuide } = useOnboardingStore();
|
||||||
|
const { activeSite } = useSiteStore();
|
||||||
|
|
||||||
|
if (!isGuideVisible) return null;
|
||||||
|
|
||||||
|
const hasSite = !!activeSite;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-8">
|
||||||
|
<Card className="rounded-2xl border-2 border-orange-200 bg-gradient-to-br from-orange-50 to-white dark:from-orange-950/20 dark:to-gray-900 dark:border-orange-800 p-6 md:p-8">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-start justify-between mb-6">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-3 mb-2">
|
||||||
|
<div className="size-12 rounded-xl bg-gradient-to-br from-orange-500 to-orange-600 flex items-center justify-center text-white shadow-lg">
|
||||||
|
<BoltIcon className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
|
Welcome to IGNY8
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
||||||
|
Your complete AI-powered content creation workflow
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={dismissGuide}
|
||||||
|
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
|
||||||
|
>
|
||||||
|
<CloseIcon className="w-5 h-5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Workflow Options */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||||
|
{/* Build New Site */}
|
||||||
|
<Card className="p-6 border-2 border-blue-200 dark:border-blue-800 hover:border-blue-400 dark:hover:border-blue-600 transition-colors">
|
||||||
|
<div className="flex items-start gap-4 mb-4">
|
||||||
|
<div className="size-12 rounded-xl bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white flex-shrink-0">
|
||||||
|
<GridIcon className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-lg font-bold text-gray-900 dark:text-white mb-1">
|
||||||
|
Build New Site
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Create a new website from scratch with IGNY8
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/sites/builder?type=wordpress');
|
||||||
|
dismissGuide();
|
||||||
|
}}
|
||||||
|
className="w-full flex items-center justify-between p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-blue-400 dark:hover:border-blue-600 bg-white dark:bg-gray-800 transition-colors group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<PlugInIcon className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
WordPress Self-Hosted Site
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Build and sync to your WordPress installation
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ArrowRightIcon className="w-5 h-5 text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/sites/builder?type=igny8');
|
||||||
|
dismissGuide();
|
||||||
|
}}
|
||||||
|
className="w-full flex items-center justify-between p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-blue-400 dark:hover:border-blue-600 bg-white dark:bg-gray-800 transition-colors group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<GridIcon className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
IGNY8-Powered IGNY8-Hosted Site
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Fully managed site hosted by IGNY8
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ArrowRightIcon className="w-5 h-5 text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Integrate Existing Site */}
|
||||||
|
<Card className="p-6 border-2 border-green-200 dark:border-green-800 hover:border-green-400 dark:hover:border-green-600 transition-colors">
|
||||||
|
<div className="flex items-start gap-4 mb-4">
|
||||||
|
<div className="size-12 rounded-xl bg-gradient-to-br from-green-500 to-green-600 flex items-center justify-center text-white flex-shrink-0">
|
||||||
|
<PlugInIcon className="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-lg font-bold text-gray-900 dark:text-white mb-1">
|
||||||
|
Integrate Existing Site
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Connect your existing website to IGNY8
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/sites?action=integrate&platform=wordpress');
|
||||||
|
dismissGuide();
|
||||||
|
}}
|
||||||
|
className="w-full flex items-center justify-between p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-green-400 dark:hover:border-green-600 bg-white dark:bg-gray-800 transition-colors group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<PlugInIcon className="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
Integrate WordPress/Shopify
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Sync content with your existing site
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ArrowRightIcon className="w-5 h-5 text-gray-400 group-hover:text-green-600 dark:group-hover:text-green-400 transition-colors" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/sites?action=integrate&platform=custom');
|
||||||
|
dismissGuide();
|
||||||
|
}}
|
||||||
|
className="w-full flex items-center justify-between p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-green-400 dark:hover:border-green-600 bg-white dark:bg-gray-800 transition-colors group"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<FileIcon className="w-5 h-5 text-green-600 dark:text-green-400" />
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">
|
||||||
|
Custom Site
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Connect any custom website
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ArrowRightIcon className="w-5 h-5 text-gray-400 group-hover:text-green-600 dark:group-hover:text-green-400 transition-colors" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Workflow Steps */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||||
|
Your Content Creation Workflow
|
||||||
|
</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
{[
|
||||||
|
{ icon: <ListIcon />, label: 'Discover Keywords', gradient: 'from-blue-500 to-blue-600', path: '/planner/keywords' },
|
||||||
|
{ icon: <GroupIcon />, label: 'Cluster Keywords', gradient: 'from-purple-500 to-purple-600', path: '/planner/clusters' },
|
||||||
|
{ icon: <BoltIcon />, label: 'Generate Ideas', gradient: 'from-orange-500 to-orange-600', path: '/planner/ideas' },
|
||||||
|
{ icon: <FileTextIcon />, label: 'Create Content', gradient: 'from-green-500 to-green-600', path: '/writer/content' },
|
||||||
|
].map((step, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
onClick={() => navigate(step.path)}
|
||||||
|
className="flex items-center gap-3 p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-[var(--color-primary)] dark:hover:border-[var(--color-primary)] bg-white dark:bg-gray-800 transition-colors group"
|
||||||
|
>
|
||||||
|
<div className={`size-10 rounded-lg bg-gradient-to-br ${step.gradient} flex items-center justify-center text-white flex-shrink-0`}>
|
||||||
|
{React.cloneElement(step.icon as React.ReactElement, { className: 'w-5 h-5' })}
|
||||||
|
</div>
|
||||||
|
<div className="text-left">
|
||||||
|
<div className="font-semibold text-sm text-gray-900 dark:text-white group-hover:text-[var(--color-primary)]">
|
||||||
|
{step.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress Indicator (if user has started) */}
|
||||||
|
{hasSite && (
|
||||||
|
<div className="flex items-center gap-2 p-4 rounded-lg bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800">
|
||||||
|
<CheckCircleIcon className="w-5 h-5 text-blue-600 dark:text-blue-400 flex-shrink-0" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-semibold text-sm text-blue-900 dark:text-blue-100">
|
||||||
|
Great! You've created your first site
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-blue-700 dark:text-blue-300 mt-1">
|
||||||
|
Continue with keyword research and content planning
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/planner/keywords');
|
||||||
|
dismissGuide();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Get Started
|
||||||
|
<ArrowRightIcon className="w-4 h-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Footer Actions */}
|
||||||
|
<div className="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
You can always access this guide from the header
|
||||||
|
</p>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/sites');
|
||||||
|
dismissGuide();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View All Sites
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
navigate('/planner/keywords');
|
||||||
|
dismissGuide();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Start Planning
|
||||||
|
<ArrowRightIcon className="w-4 h-4 ml-2" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -7,6 +7,25 @@ import NotificationDropdown from "../components/header/NotificationDropdown";
|
|||||||
import UserDropdown from "../components/header/UserDropdown";
|
import UserDropdown from "../components/header/UserDropdown";
|
||||||
import { HeaderMetrics } from "../components/header/HeaderMetrics";
|
import { HeaderMetrics } from "../components/header/HeaderMetrics";
|
||||||
import ResourceDebugToggle from "../components/debug/ResourceDebugToggle";
|
import ResourceDebugToggle from "../components/debug/ResourceDebugToggle";
|
||||||
|
import { useOnboardingStore } from "../store/onboardingStore";
|
||||||
|
import Button from "../components/ui/button/Button";
|
||||||
|
import { BoltIcon } from "../icons";
|
||||||
|
|
||||||
|
const ShowGuideButton: React.FC = () => {
|
||||||
|
const { toggleGuide, isGuideVisible } = useOnboardingStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={toggleGuide}
|
||||||
|
className="bg-orange-500 hover:bg-orange-600 text-white border-orange-500 hover:border-orange-600"
|
||||||
|
>
|
||||||
|
<BoltIcon className="w-4 h-4 mr-2" />
|
||||||
|
{isGuideVisible ? 'Hide Guide' : 'Show Guide'}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const AppHeader: React.FC = () => {
|
const AppHeader: React.FC = () => {
|
||||||
const [isApplicationMenuOpen, setApplicationMenuOpen] = useState(false);
|
const [isApplicationMenuOpen, setApplicationMenuOpen] = useState(false);
|
||||||
@@ -161,6 +180,8 @@ const AppHeader: React.FC = () => {
|
|||||||
<div className="flex items-center gap-2 2xsm:gap-3">
|
<div className="flex items-center gap-2 2xsm:gap-3">
|
||||||
{/* <!-- Header Metrics (conditional) --> */}
|
{/* <!-- Header Metrics (conditional) --> */}
|
||||||
<HeaderMetrics />
|
<HeaderMetrics />
|
||||||
|
{/* <!-- Show Guide Button (Orange) --> */}
|
||||||
|
<ShowGuideButton />
|
||||||
{/* <!-- Dark Mode Toggler --> */}
|
{/* <!-- Dark Mode Toggler --> */}
|
||||||
<ThemeToggleButton />
|
<ThemeToggleButton />
|
||||||
{/* <!-- Resource Debug Toggle (Admin only) --> */}
|
{/* <!-- Resource Debug Toggle (Admin only) --> */}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import UsageChartWidget from "../../components/dashboard/UsageChartWidget";
|
|||||||
import EnhancedMetricCard from "../../components/dashboard/EnhancedMetricCard";
|
import EnhancedMetricCard from "../../components/dashboard/EnhancedMetricCard";
|
||||||
import ComponentCard from "../../components/common/ComponentCard";
|
import ComponentCard from "../../components/common/ComponentCard";
|
||||||
import PageHeader from "../../components/common/PageHeader";
|
import PageHeader from "../../components/common/PageHeader";
|
||||||
|
import WorkflowGuide from "../../components/onboarding/WorkflowGuide";
|
||||||
|
import { useOnboardingStore } from "../../store/onboardingStore";
|
||||||
import { Card } from "../../components/ui/card";
|
import { Card } from "../../components/ui/card";
|
||||||
import { ProgressBar } from "../../components/ui/progress";
|
import { ProgressBar } from "../../components/ui/progress";
|
||||||
import { ApexOptions } from "apexcharts";
|
import { ApexOptions } from "apexcharts";
|
||||||
@@ -115,11 +117,19 @@ export default function Home() {
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const { activeSite } = useSiteStore();
|
const { activeSite } = useSiteStore();
|
||||||
const { activeSector } = useSectorStore();
|
const { activeSector } = useSectorStore();
|
||||||
|
const { isGuideDismissed, showGuide } = useOnboardingStore();
|
||||||
|
|
||||||
const [insights, setInsights] = useState<AppInsights | null>(null);
|
const [insights, setInsights] = useState<AppInsights | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
|
const [lastUpdated, setLastUpdated] = useState<Date>(new Date());
|
||||||
|
|
||||||
|
// Show guide on first visit if not dismissed
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isGuideDismissed) {
|
||||||
|
showGuide();
|
||||||
|
}
|
||||||
|
}, [isGuideDismissed, showGuide]);
|
||||||
|
|
||||||
const appModules = [
|
const appModules = [
|
||||||
{
|
{
|
||||||
title: "Planner",
|
title: "Planner",
|
||||||
@@ -323,6 +333,9 @@ export default function Home() {
|
|||||||
onRefresh={fetchAppInsights}
|
onRefresh={fetchAppInsights}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Welcome/Guide Screen - Inline at top */}
|
||||||
|
<WorkflowGuide />
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<div className="bg-gradient-to-r from-brand-500 to-purple-600 rounded-2xl p-8 md:p-12 text-white relative overflow-hidden">
|
<div className="bg-gradient-to-r from-brand-500 to-purple-600 rounded-2xl p-8 md:p-12 text-white relative overflow-hidden">
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
/**
|
/**
|
||||||
* Site List View
|
* Site List View
|
||||||
* Phase 7: UI Components & Prompt Management
|
* Refactored to use TablePageTemplate with table view as default
|
||||||
* Advanced site list with filters and search
|
* Supports table and grid view toggle
|
||||||
*/
|
*/
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import PageMeta from '../../components/common/PageMeta';
|
import PageMeta from '../../components/common/PageMeta';
|
||||||
import PageHeader from '../../components/common/PageHeader';
|
import PageHeader from '../../components/common/PageHeader';
|
||||||
|
import TablePageTemplate from '../../templates/TablePageTemplate';
|
||||||
import { Card } from '../../components/ui/card';
|
import { Card } from '../../components/ui/card';
|
||||||
import Button from '../../components/ui/button/Button';
|
import Button from '../../components/ui/button/Button';
|
||||||
import SelectDropdown from '../../components/form/SelectDropdown';
|
|
||||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
|
||||||
import { fetchAPI } from '../../services/api';
|
|
||||||
import SiteTypeBadge from '../../components/sites/SiteTypeBadge';
|
|
||||||
import Badge from '../../components/ui/badge/Badge';
|
import Badge from '../../components/ui/badge/Badge';
|
||||||
import FormModal, { FormField } from '../../components/common/FormModal';
|
import FormModal, { FormField } from '../../components/common/FormModal';
|
||||||
import Alert from '../../components/ui/alert/Alert';
|
import Alert from '../../components/ui/alert/Alert';
|
||||||
import Switch from '../../components/form/switch/Switch';
|
import Switch from '../../components/form/switch/Switch';
|
||||||
|
import ViewToggle from '../../components/common/ViewToggle';
|
||||||
import {
|
import {
|
||||||
PlusIcon,
|
PlusIcon,
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
@@ -25,8 +23,8 @@ import {
|
|||||||
GridIcon,
|
GridIcon,
|
||||||
PlugInIcon,
|
PlugInIcon,
|
||||||
FileIcon,
|
FileIcon,
|
||||||
MoreDotIcon,
|
PageIcon,
|
||||||
PageIcon
|
TableIcon
|
||||||
} from '../../icons';
|
} from '../../icons';
|
||||||
import {
|
import {
|
||||||
fetchSites,
|
fetchSites,
|
||||||
@@ -39,7 +37,10 @@ import {
|
|||||||
fetchSiteSectors,
|
fetchSiteSectors,
|
||||||
Site as SiteType,
|
Site as SiteType,
|
||||||
Industry,
|
Industry,
|
||||||
|
fetchAPI,
|
||||||
} from '../../services/api';
|
} from '../../services/api';
|
||||||
|
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||||
|
import SiteTypeBadge from '../../components/sites/SiteTypeBadge';
|
||||||
|
|
||||||
interface Site extends SiteType {
|
interface Site extends SiteType {
|
||||||
page_count?: number;
|
page_count?: number;
|
||||||
@@ -51,18 +52,20 @@ interface Site extends SiteType {
|
|||||||
active_sectors_count?: number;
|
active_sectors_count?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ViewType = 'table' | 'grid';
|
||||||
|
|
||||||
export default function SiteList() {
|
export default function SiteList() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const [sites, setSites] = useState<Site[]>([]);
|
const [sites, setSites] = useState<Site[]>([]);
|
||||||
const [filteredSites, setFilteredSites] = useState<Site[]>([]);
|
const [filteredSites, setFilteredSites] = useState<Site[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [viewType, setViewType] = useState<ViewType>('table');
|
||||||
|
|
||||||
// Site Management Modals
|
// Site Management Modals
|
||||||
const [selectedSite, setSelectedSite] = useState<Site | null>(null);
|
const [selectedSite, setSelectedSite] = useState<Site | null>(null);
|
||||||
const [showSiteModal, setShowSiteModal] = useState(false);
|
const [showSiteModal, setShowSiteModal] = useState(false);
|
||||||
const [showSectorsModal, setShowSectorsModal] = useState(false);
|
const [showSectorsModal, setShowSectorsModal] = useState(false);
|
||||||
const [showDetailsModal, setShowDetailsModal] = useState(false);
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [togglingSiteId, setTogglingSiteId] = useState<number | null>(null);
|
const [togglingSiteId, setTogglingSiteId] = useState<number | null>(null);
|
||||||
const [industries, setIndustries] = useState<Industry[]>([]);
|
const [industries, setIndustries] = useState<Industry[]>([]);
|
||||||
@@ -168,7 +171,7 @@ export default function SiteList() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Integration filter (has integrations or not)
|
// Integration filter
|
||||||
if (integrationFilter === 'has_integrations') {
|
if (integrationFilter === 'has_integrations') {
|
||||||
filtered = filtered.filter((site) => (site.integration_count || 0) > 0);
|
filtered = filtered.filter((site) => (site.integration_count || 0) > 0);
|
||||||
} else if (integrationFilter === 'no_integrations') {
|
} else if (integrationFilter === 'no_integrations') {
|
||||||
@@ -189,18 +192,7 @@ export default function SiteList() {
|
|||||||
setShowSiteModal(true);
|
setShowSiteModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleIntegration = (siteId: number) => {
|
const handleEdit = (site: Site) => {
|
||||||
navigate(`/sites/${siteId}/settings?tab=integrations`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEdit = (siteId: number) => {
|
|
||||||
const site = sites.find(s => s.id === siteId);
|
|
||||||
if (site) {
|
|
||||||
handleEditSite(site);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEditSite = (site: Site) => {
|
|
||||||
setSelectedSite(site);
|
setSelectedSite(site);
|
||||||
setFormData({
|
setFormData({
|
||||||
name: site.name || '',
|
name: site.name || '',
|
||||||
@@ -211,13 +203,10 @@ export default function SiteList() {
|
|||||||
setShowSiteModal(true);
|
setShowSiteModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSettings = (siteId: number) => {
|
const handleSettings = (site: Site) => {
|
||||||
const site = sites.find(s => s.id === siteId);
|
|
||||||
if (site) {
|
|
||||||
setSelectedSite(site);
|
setSelectedSite(site);
|
||||||
setShowSectorsModal(true);
|
setShowSectorsModal(true);
|
||||||
loadSiteSectors(site);
|
loadSiteSectors(site);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggle = async (siteId: number, enabled: boolean) => {
|
const handleToggle = async (siteId: number, enabled: boolean) => {
|
||||||
@@ -268,45 +257,6 @@ export default function SiteList() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDetails = (site: Site) => {
|
|
||||||
setSelectedSite(site);
|
|
||||||
setFormData({
|
|
||||||
name: site.name || '',
|
|
||||||
domain: site.domain || '',
|
|
||||||
description: site.description || '',
|
|
||||||
is_active: site.is_active || false,
|
|
||||||
});
|
|
||||||
setShowDetailsModal(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveDetails = async () => {
|
|
||||||
if (!selectedSite) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
setIsSaving(true);
|
|
||||||
const normalizedFormData = {
|
|
||||||
...formData,
|
|
||||||
domain: formData.domain ? normalizeDomain(formData.domain) : formData.domain,
|
|
||||||
};
|
|
||||||
await updateSite(selectedSite.id, normalizedFormData);
|
|
||||||
toast.success('Site updated successfully');
|
|
||||||
setShowDetailsModal(false);
|
|
||||||
await loadSites();
|
|
||||||
} catch (error: any) {
|
|
||||||
toast.error(`Failed to update site: ${error.message}`);
|
|
||||||
} finally {
|
|
||||||
setIsSaving(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeDomain = (domain: string): string => {
|
|
||||||
if (!domain || !domain.trim()) return domain;
|
|
||||||
const trimmed = domain.trim();
|
|
||||||
if (trimmed.startsWith('https://')) return trimmed;
|
|
||||||
if (trimmed.startsWith('http://')) return trimmed.replace('http://', 'https://');
|
|
||||||
return `https://${trimmed}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSaveSite = async () => {
|
const handleSaveSite = async () => {
|
||||||
try {
|
try {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
@@ -345,6 +295,14 @@ export default function SiteList() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizeDomain = (domain: string): string => {
|
||||||
|
if (!domain || !domain.trim()) return domain;
|
||||||
|
const trimmed = domain.trim();
|
||||||
|
if (trimmed.startsWith('https://')) return trimmed;
|
||||||
|
if (trimmed.startsWith('http://')) return trimmed.replace('http://', 'https://');
|
||||||
|
return `https://${trimmed}`;
|
||||||
|
};
|
||||||
|
|
||||||
const handleSelectSectors = async () => {
|
const handleSelectSectors = async () => {
|
||||||
if (!selectedSite || !selectedIndustry || selectedSectors.length === 0) {
|
if (!selectedSite || !selectedIndustry || selectedSectors.length === 0) {
|
||||||
toast.error('Please select an industry and at least one sector');
|
toast.error('Please select an industry and at least one sector');
|
||||||
@@ -373,19 +331,11 @@ export default function SiteList() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteSite = async (site: Site) => {
|
const handleDeleteSite = async (siteId: number) => {
|
||||||
if (!window.confirm(`Are you sure you want to delete "${site.name}"? This action cannot be undone.`)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteSite(site.id);
|
await deleteSite(siteId);
|
||||||
toast.success('Site deleted successfully');
|
toast.success('Site deleted successfully');
|
||||||
await loadSites();
|
await loadSites();
|
||||||
if (showDetailsModal) {
|
|
||||||
setShowDetailsModal(false);
|
|
||||||
setSelectedSite(null);
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.error(`Failed to delete site: ${error.message}`);
|
toast.error(`Failed to delete site: ${error.message}`);
|
||||||
}
|
}
|
||||||
@@ -440,24 +390,6 @@ export default function SiteList() {
|
|||||||
return industry?.sectors || [];
|
return industry?.sectors || [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleView = (siteId: number) => {
|
|
||||||
navigate(`/sites/${siteId}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = async (siteId: number) => {
|
|
||||||
if (!confirm('Are you sure you want to delete this site?')) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fetchAPI(`/v1/auth/sites/${siteId}/`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
toast.success('Site deleted successfully');
|
|
||||||
loadSites();
|
|
||||||
} catch (error: any) {
|
|
||||||
toast.error(`Failed to delete site: ${error.message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const clearFilters = () => {
|
const clearFilters = () => {
|
||||||
setSearchTerm('');
|
setSearchTerm('');
|
||||||
setSiteTypeFilter('');
|
setSiteTypeFilter('');
|
||||||
@@ -487,9 +419,6 @@ export default function SiteList() {
|
|||||||
{ value: '', label: 'All Status' },
|
{ value: '', label: 'All Status' },
|
||||||
{ value: 'active', label: 'Active' },
|
{ value: 'active', label: 'Active' },
|
||||||
{ value: 'inactive', label: 'Inactive' },
|
{ value: 'inactive', label: 'Inactive' },
|
||||||
{ value: 'active', label: 'Active Status' },
|
|
||||||
{ value: 'inactive', label: 'Inactive Status' },
|
|
||||||
{ value: 'suspended', label: 'Suspended' },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const INTEGRATION_OPTIONS = [
|
const INTEGRATION_OPTIONS = [
|
||||||
@@ -498,165 +427,90 @@ export default function SiteList() {
|
|||||||
{ value: 'no_integrations', label: 'No Integrations' },
|
{ value: 'no_integrations', label: 'No Integrations' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const hasActiveFilters = searchTerm || siteTypeFilter || hostingTypeFilter || statusFilter || integrationFilter;
|
// Table columns configuration
|
||||||
|
const tableColumns = useMemo(() => [
|
||||||
if (loading) {
|
{
|
||||||
return (
|
key: 'name',
|
||||||
<div className="p-6">
|
label: 'Site Name',
|
||||||
<PageMeta title="Site List" />
|
sortable: true,
|
||||||
<div className="flex items-center justify-center h-64">
|
render: (value: string, row: Site) => (
|
||||||
<div className="text-gray-500">Loading sites...</div>
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="size-10 rounded-lg bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white flex-shrink-0">
|
||||||
|
<GridIcon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold text-gray-900 dark:text-white">{row.name}</div>
|
||||||
|
{row.domain && (
|
||||||
|
<div className="text-xs text-gray-500 dark:text-gray-400">{row.domain}</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
),
|
||||||
}
|
},
|
||||||
|
{
|
||||||
return (
|
key: 'hosting_type',
|
||||||
<div className="p-6">
|
label: 'Hosting',
|
||||||
<PageMeta title="Sites Management - IGNY8" />
|
sortable: true,
|
||||||
<PageHeader
|
render: (value: string, row: Site) => <SiteTypeBadge hostingType={row.hosting_type} />,
|
||||||
title="Sites Management"
|
},
|
||||||
badge={{ icon: <GridIcon />, color: 'blue' }}
|
{
|
||||||
/>
|
key: 'industry_name',
|
||||||
|
label: 'Industry',
|
||||||
<div className="mb-6">
|
sortable: false,
|
||||||
<div className="flex items-center justify-between mb-4">
|
render: (value: string, row: Site) => (
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
row.industry_name ? (
|
||||||
Manage your sites, configure industries, and select sectors. Multiple sites can be active simultaneously.
|
<Badge variant="light" color="info" size="sm">
|
||||||
</p>
|
{row.industry_name}
|
||||||
<div className="flex gap-2">
|
</Badge>
|
||||||
<Button onClick={() => navigate('/sites/builder')} variant="outline">
|
) : (
|
||||||
<PlusIcon className="w-4 h-4 mr-2" />
|
<span className="text-gray-400">-</span>
|
||||||
Create with Builder
|
)
|
||||||
</Button>
|
),
|
||||||
<Button onClick={handleCreateSite} variant="primary">
|
},
|
||||||
<PlusIcon className="w-4 h-4 mr-2" />
|
{
|
||||||
Add Site
|
key: 'active_sectors_count',
|
||||||
</Button>
|
label: 'Sectors',
|
||||||
</div>
|
sortable: false,
|
||||||
</div>
|
render: (value: number, row: Site) => (
|
||||||
|
<Badge variant="light" color="info" size="sm">
|
||||||
{/* Info Alert */}
|
{row.active_sectors_count || 0} / 5
|
||||||
<Alert
|
</Badge>
|
||||||
variant="info"
|
),
|
||||||
title="Sites Configuration"
|
},
|
||||||
message="Each site can have up to 5 sectors selected from 15 major industries. Keywords and clusters are automatically associated with sectors. Multiple sites can be active simultaneously."
|
{
|
||||||
/>
|
key: 'integration_count',
|
||||||
</div>
|
label: 'Integrations',
|
||||||
|
sortable: false,
|
||||||
{/* Filters */}
|
render: (value: number, row: Site) => (
|
||||||
<Card className="p-4 mb-6">
|
row.integration_count && row.integration_count > 0 ? (
|
||||||
<div className="flex items-center gap-2 mb-4">
|
<Badge variant="soft" color="success" size="sm">
|
||||||
<div className="flex-shrink-0 size-8 rounded-lg bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-md">
|
{row.integration_count}
|
||||||
<GridIcon className="h-4 w-4" />
|
</Badge>
|
||||||
</div>
|
) : (
|
||||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
|
<span className="text-gray-400">-</span>
|
||||||
Filters
|
)
|
||||||
</h2>
|
),
|
||||||
{hasActiveFilters && (
|
},
|
||||||
<Button
|
{
|
||||||
variant="ghost"
|
key: 'is_active',
|
||||||
|
label: 'Status',
|
||||||
|
sortable: true,
|
||||||
|
render: (value: boolean, row: Site) => (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge
|
||||||
|
variant={row.is_active ? "soft" : "light"}
|
||||||
|
color={row.is_active ? "success" : "gray"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={clearFilters}
|
|
||||||
className="ml-auto"
|
|
||||||
>
|
>
|
||||||
Clear Filters
|
{row.is_active ? 'Active' : 'Inactive'}
|
||||||
</Button>
|
</Badge>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
], []);
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
// Grid view component
|
||||||
{/* Search */}
|
const renderGridView = () => (
|
||||||
<div className="lg:col-span-2">
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Search
|
|
||||||
</label>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2">
|
|
||||||
<GridIcon className="w-4 h-4 text-gray-400" />
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
|
||||||
placeholder="Search sites..."
|
|
||||||
className="w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Site Type */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Site Type
|
|
||||||
</label>
|
|
||||||
<SelectDropdown
|
|
||||||
options={SITE_TYPES}
|
|
||||||
value={siteTypeFilter}
|
|
||||||
onChange={(value) => setSiteTypeFilter(value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Hosting Type */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Hosting
|
|
||||||
</label>
|
|
||||||
<SelectDropdown
|
|
||||||
options={HOSTING_TYPES}
|
|
||||||
value={hostingTypeFilter}
|
|
||||||
onChange={(value) => setHostingTypeFilter(value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Status */}
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Status
|
|
||||||
</label>
|
|
||||||
<SelectDropdown
|
|
||||||
options={STATUS_OPTIONS}
|
|
||||||
value={statusFilter}
|
|
||||||
onChange={(value) => setStatusFilter(value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4">
|
|
||||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
||||||
Integrations
|
|
||||||
</label>
|
|
||||||
<SelectDropdown
|
|
||||||
options={INTEGRATION_OPTIONS}
|
|
||||||
value={integrationFilter}
|
|
||||||
onChange={(value) => setIntegrationFilter(value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Results Count */}
|
|
||||||
<div className="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
Showing {filteredSites.length} of {sites.length} sites
|
|
||||||
{hasActiveFilters && ' (filtered)'}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sites List */}
|
|
||||||
{filteredSites.length === 0 ? (
|
|
||||||
<Card className="p-12 text-center">
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
|
||||||
{hasActiveFilters ? 'No sites match your filters' : 'No sites created yet'}
|
|
||||||
</p>
|
|
||||||
{hasActiveFilters ? (
|
|
||||||
<Button onClick={clearFilters} variant="outline">
|
|
||||||
Clear Filters
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button onClick={handleCreateSite} variant="primary">
|
|
||||||
Create Your First Site
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
) : (
|
|
||||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3">
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3">
|
||||||
{filteredSites.map((site) => (
|
{filteredSites.map((site) => (
|
||||||
<Card key={site.id} className="rounded-xl border-2 border-slate-200 bg-white dark:border-gray-800 dark:bg-white/3 hover:border-[var(--color-primary)] hover:shadow-lg transition-all">
|
<Card key={site.id} className="rounded-xl border-2 border-slate-200 bg-white dark:border-gray-800 dark:bg-white/3 hover:border-[var(--color-primary)] hover:shadow-lg transition-all">
|
||||||
@@ -691,26 +545,23 @@ export default function SiteList() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Status Text and Circle - Same row */}
|
<div className="absolute top-5 right-5">
|
||||||
<div className="absolute top-5 right-5 flex items-center gap-2">
|
<Badge
|
||||||
<span className={`text-sm ${site.is_active ? 'text-green-600 dark:text-green-400 font-bold' : 'text-gray-400 dark:text-gray-500'} transition-colors duration-200`}>
|
variant={site.is_active ? "soft" : "light"}
|
||||||
|
color={site.is_active ? "success" : "gray"}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
{site.is_active ? 'Active' : 'Inactive'}
|
{site.is_active ? 'Active' : 'Inactive'}
|
||||||
</span>
|
</Badge>
|
||||||
<div
|
|
||||||
className={`w-[25px] h-[25px] rounded-full ${site.is_active ? 'bg-green-500 dark:bg-green-600' : 'bg-gray-400 dark:bg-gray-500'} transition-colors duration-200`}
|
|
||||||
title={site.is_active ? 'Active site' : 'Inactive site'}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="border-t border-gray-200 p-5 dark:border-gray-800">
|
<div className="border-t border-gray-200 p-5 dark:border-gray-800">
|
||||||
{/* Quick Actions */}
|
|
||||||
<div className="grid grid-cols-3 gap-2 mb-3">
|
<div className="grid grid-cols-3 gap-2 mb-3">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleView(site.id)}
|
onClick={() => navigate(`/sites/${site.id}`)}
|
||||||
className="w-full justify-center text-xs"
|
className="w-full justify-center text-xs"
|
||||||
title="View Dashboard"
|
|
||||||
>
|
>
|
||||||
<EyeIcon className="w-3 h-3 mr-1" />
|
<EyeIcon className="w-3 h-3 mr-1" />
|
||||||
Dashboard
|
Dashboard
|
||||||
@@ -720,7 +571,6 @@ export default function SiteList() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => navigate(`/sites/${site.id}/content`)}
|
onClick={() => navigate(`/sites/${site.id}/content`)}
|
||||||
className="w-full justify-center text-xs"
|
className="w-full justify-center text-xs"
|
||||||
title="View Content"
|
|
||||||
>
|
>
|
||||||
<FileIcon className="w-3 h-3 mr-1" />
|
<FileIcon className="w-3 h-3 mr-1" />
|
||||||
Content
|
Content
|
||||||
@@ -730,21 +580,17 @@ export default function SiteList() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => navigate(`/sites/${site.id}/pages`)}
|
onClick={() => navigate(`/sites/${site.id}/pages`)}
|
||||||
className="w-full justify-center text-xs"
|
className="w-full justify-center text-xs"
|
||||||
title="Manage Pages"
|
|
||||||
>
|
>
|
||||||
<PageIcon className="w-3 h-3 mr-1" />
|
<PageIcon className="w-3 h-3 mr-1" />
|
||||||
Pages
|
Pages
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Secondary Actions */}
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handleSettings(site.id)}
|
onClick={() => handleSettings(site)}
|
||||||
className="shadow-theme-xs inline-flex h-9 items-center justify-center rounded-lg border border-gray-300 text-gray-700 dark:border-gray-700 dark:text-gray-400 px-3"
|
|
||||||
title="Configure Sectors"
|
title="Configure Sectors"
|
||||||
>
|
>
|
||||||
<GridIcon className="w-4 h-4 mr-1" />
|
<GridIcon className="w-4 h-4 mr-1" />
|
||||||
@@ -754,7 +600,6 @@ export default function SiteList() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => navigate(`/sites/${site.id}/settings`)}
|
onClick={() => navigate(`/sites/${site.id}/settings`)}
|
||||||
className="shadow-theme-xs inline-flex h-9 items-center justify-center rounded-lg border border-gray-300 text-gray-700 dark:border-gray-700 dark:text-gray-400 px-3"
|
|
||||||
title="Site Settings"
|
title="Site Settings"
|
||||||
>
|
>
|
||||||
<PlugInIcon className="w-4 h-4 mr-1" />
|
<PlugInIcon className="w-4 h-4 mr-1" />
|
||||||
@@ -771,6 +616,235 @@ export default function SiteList() {
|
|||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasActiveFilters = searchTerm || siteTypeFilter || hostingTypeFilter || statusFilter || integrationFilter;
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<PageMeta title="Site List" />
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<div className="text-gray-500">Loading sites...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6">
|
||||||
|
<PageMeta title="Sites Management - IGNY8" />
|
||||||
|
<PageHeader
|
||||||
|
title="Sites Management"
|
||||||
|
badge={{ icon: <GridIcon />, color: 'blue' }}
|
||||||
|
hideSiteSector={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Info Alert */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<Alert
|
||||||
|
variant="info"
|
||||||
|
title="Sites Configuration"
|
||||||
|
message="Each site can have up to 5 sectors selected from 15 major industries. Keywords and clusters are automatically associated with sectors. Multiple sites can be active simultaneously."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Custom Header Actions - Create buttons and view toggle */}
|
||||||
|
<div className="flex items-center justify-between mb-6">
|
||||||
|
<div className="flex-1"></div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button onClick={() => navigate('/sites/builder')} variant="outline">
|
||||||
|
<PlusIcon className="w-4 h-4 mr-2" />
|
||||||
|
Create with Builder
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleCreateSite} variant="primary">
|
||||||
|
<PlusIcon className="w-4 h-4 mr-2" />
|
||||||
|
Add Site
|
||||||
|
</Button>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setViewType('table')}
|
||||||
|
className={`inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
||||||
|
viewType === 'table'
|
||||||
|
? 'bg-white text-gray-900 dark:bg-gray-800 dark:text-white shadow-sm border border-gray-200 dark:border-gray-700'
|
||||||
|
: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
||||||
|
}`}
|
||||||
|
title="Table View"
|
||||||
|
>
|
||||||
|
<TableIcon className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">Table</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewType('grid')}
|
||||||
|
className={`inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
||||||
|
viewType === 'grid'
|
||||||
|
? 'bg-white text-gray-900 dark:bg-gray-800 dark:text-white shadow-sm border border-gray-200 dark:border-gray-700'
|
||||||
|
: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'
|
||||||
|
}`}
|
||||||
|
title="Grid View"
|
||||||
|
>
|
||||||
|
<GridIcon className="w-4 h-4" />
|
||||||
|
<span className="hidden sm:inline">Grid</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Table View */}
|
||||||
|
{viewType === 'table' ? (
|
||||||
|
<TablePageTemplate
|
||||||
|
columns={tableColumns}
|
||||||
|
data={filteredSites}
|
||||||
|
loading={loading}
|
||||||
|
showContent={!loading}
|
||||||
|
filters={[
|
||||||
|
{
|
||||||
|
key: 'search',
|
||||||
|
label: 'Search',
|
||||||
|
type: 'text',
|
||||||
|
placeholder: 'Search sites...',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'site_type',
|
||||||
|
label: 'Site Type',
|
||||||
|
type: 'select',
|
||||||
|
options: SITE_TYPES,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'hosting_type',
|
||||||
|
label: 'Hosting',
|
||||||
|
type: 'select',
|
||||||
|
options: HOSTING_TYPES,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'status',
|
||||||
|
label: 'Status',
|
||||||
|
type: 'select',
|
||||||
|
options: STATUS_OPTIONS,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'integration',
|
||||||
|
label: 'Integrations',
|
||||||
|
type: 'select',
|
||||||
|
options: INTEGRATION_OPTIONS,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
filterValues={{
|
||||||
|
search: searchTerm,
|
||||||
|
site_type: siteTypeFilter,
|
||||||
|
hosting_type: hostingTypeFilter,
|
||||||
|
status: statusFilter,
|
||||||
|
integration: integrationFilter,
|
||||||
|
}}
|
||||||
|
onFilterChange={(key, value) => {
|
||||||
|
if (key === 'search') setSearchTerm(value);
|
||||||
|
else if (key === 'site_type') setSiteTypeFilter(value);
|
||||||
|
else if (key === 'hosting_type') setHostingTypeFilter(value);
|
||||||
|
else if (key === 'status') setStatusFilter(value);
|
||||||
|
else if (key === 'integration') setIntegrationFilter(value);
|
||||||
|
}}
|
||||||
|
onFilterReset={clearFilters}
|
||||||
|
onEdit={(row) => handleEdit(row)}
|
||||||
|
onDelete={async (id) => {
|
||||||
|
await handleDeleteSite(id);
|
||||||
|
}}
|
||||||
|
getItemDisplayName={(row) => row.name}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{/* Filters for Grid View */}
|
||||||
|
<Card className="p-4 mb-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-4">
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Search
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
placeholder="Search sites..."
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Site Type
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={siteTypeFilter}
|
||||||
|
onChange={(e) => setSiteTypeFilter(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||||
|
>
|
||||||
|
{SITE_TYPES.map(opt => (
|
||||||
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Hosting
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={hostingTypeFilter}
|
||||||
|
onChange={(e) => setHostingTypeFilter(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||||
|
>
|
||||||
|
{HOSTING_TYPES.map(opt => (
|
||||||
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||||
|
Status
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={statusFilter}
|
||||||
|
onChange={(e) => setStatusFilter(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||||
|
>
|
||||||
|
{STATUS_OPTIONS.map(opt => (
|
||||||
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{hasActiveFilters && (
|
||||||
|
<div className="mt-4">
|
||||||
|
<Button variant="ghost" size="sm" onClick={clearFilters}>
|
||||||
|
Clear Filters
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Results Count */}
|
||||||
|
<div className="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
Showing {filteredSites.length} of {sites.length} sites
|
||||||
|
{hasActiveFilters && ' (filtered)'}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid View */}
|
||||||
|
{filteredSites.length === 0 ? (
|
||||||
|
<Card className="p-12 text-center">
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 mb-4">
|
||||||
|
{hasActiveFilters ? 'No sites match your filters' : 'No sites created yet'}
|
||||||
|
</p>
|
||||||
|
{hasActiveFilters ? (
|
||||||
|
<Button onClick={clearFilters} variant="outline">
|
||||||
|
Clear Filters
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button onClick={handleCreateSite} variant="primary">
|
||||||
|
Create Your First Site
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
renderGridView()
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Create/Edit Site Modal */}
|
{/* Create/Edit Site Modal */}
|
||||||
@@ -896,57 +970,6 @@ export default function SiteList() {
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Site Details Modal - Editable */}
|
|
||||||
{selectedSite && (
|
|
||||||
<FormModal
|
|
||||||
isOpen={showDetailsModal}
|
|
||||||
onClose={() => {
|
|
||||||
setShowDetailsModal(false);
|
|
||||||
setSelectedSite(null);
|
|
||||||
}}
|
|
||||||
onSubmit={handleSaveDetails}
|
|
||||||
title={`Edit Site: ${selectedSite.name}`}
|
|
||||||
submitLabel="Save Changes"
|
|
||||||
fields={getSiteFormFields()}
|
|
||||||
isLoading={isSaving}
|
|
||||||
customFooter={
|
|
||||||
<div className="flex justify-between items-center pt-4 border-t border-gray-200 dark:border-gray-700">
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
onClick={() => {
|
|
||||||
if (selectedSite) {
|
|
||||||
handleDeleteSite(selectedSite);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={isSaving}
|
|
||||||
>
|
|
||||||
Delete Site
|
|
||||||
</Button>
|
|
||||||
<div className="flex gap-3">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
setShowDetailsModal(false);
|
|
||||||
setSelectedSite(null);
|
|
||||||
}}
|
|
||||||
disabled={isSaving}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
onClick={handleSaveDetails}
|
|
||||||
disabled={isSaving}
|
|
||||||
>
|
|
||||||
{isSaving ? 'Saving...' : 'Save Changes'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
33
frontend/src/store/onboardingStore.ts
Normal file
33
frontend/src/store/onboardingStore.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/**
|
||||||
|
* Onboarding Store (Zustand)
|
||||||
|
* Manages welcome/guide screen state and dismissal
|
||||||
|
*/
|
||||||
|
import { create } from 'zustand';
|
||||||
|
import { persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
interface OnboardingState {
|
||||||
|
isGuideDismissed: boolean;
|
||||||
|
isGuideVisible: boolean;
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
dismissGuide: () => void;
|
||||||
|
showGuide: () => void;
|
||||||
|
toggleGuide: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useOnboardingStore = create<OnboardingState>()(
|
||||||
|
persist<OnboardingState>(
|
||||||
|
(set) => ({
|
||||||
|
isGuideDismissed: false,
|
||||||
|
isGuideVisible: false,
|
||||||
|
|
||||||
|
dismissGuide: () => set({ isGuideDismissed: true, isGuideVisible: false }),
|
||||||
|
showGuide: () => set({ isGuideVisible: true }),
|
||||||
|
toggleGuide: () => set((state) => ({ isGuideVisible: !state.isGuideVisible })),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'onboarding-storage',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
Reference in New Issue
Block a user