From 8489b2ea48af826fce908e74c9f54a2bc4acd095 Mon Sep 17 00:00:00 2001 From: alorig <220087330+alorig@users.noreply.github.com> Date: Thu, 20 Nov 2025 09:34:54 +0500 Subject: [PATCH] 3 --- .../components/onboarding/WorkflowGuide.tsx | 275 ++++++ frontend/src/layout/AppHeader.tsx | 21 + frontend/src/pages/Dashboard/Home.tsx | 13 + frontend/src/pages/Sites/List.tsx | 795 +++++++++--------- frontend/src/store/onboardingStore.ts | 33 + 5 files changed, 751 insertions(+), 386 deletions(-) create mode 100644 frontend/src/components/onboarding/WorkflowGuide.tsx create mode 100644 frontend/src/store/onboardingStore.ts diff --git a/frontend/src/components/onboarding/WorkflowGuide.tsx b/frontend/src/components/onboarding/WorkflowGuide.tsx new file mode 100644 index 00000000..a2f51ba4 --- /dev/null +++ b/frontend/src/components/onboarding/WorkflowGuide.tsx @@ -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 ( +
+ + {/* Header */} +
+
+
+
+ +
+
+

+ Welcome to IGNY8 +

+

+ Your complete AI-powered content creation workflow +

+
+
+
+ +
+ + {/* Main Workflow Options */} +
+ {/* Build New Site */} + +
+
+ +
+
+

+ Build New Site +

+

+ Create a new website from scratch with IGNY8 +

+
+
+
+ + +
+
+ + {/* Integrate Existing Site */} + +
+
+ +
+
+

+ Integrate Existing Site +

+

+ Connect your existing website to IGNY8 +

+
+
+
+ + +
+
+
+ + {/* Workflow Steps */} +
+

+ Your Content Creation Workflow +

+
+ {[ + { icon: , label: 'Discover Keywords', gradient: 'from-blue-500 to-blue-600', path: '/planner/keywords' }, + { icon: , label: 'Cluster Keywords', gradient: 'from-purple-500 to-purple-600', path: '/planner/clusters' }, + { icon: , label: 'Generate Ideas', gradient: 'from-orange-500 to-orange-600', path: '/planner/ideas' }, + { icon: , label: 'Create Content', gradient: 'from-green-500 to-green-600', path: '/writer/content' }, + ].map((step, index) => ( + + ))} +
+
+ + {/* Progress Indicator (if user has started) */} + {hasSite && ( +
+ +
+
+ Great! You've created your first site +
+
+ Continue with keyword research and content planning +
+
+ +
+ )} + + {/* Footer Actions */} +
+

+ You can always access this guide from the header +

+
+ + +
+
+
+
+ ); +} + diff --git a/frontend/src/layout/AppHeader.tsx b/frontend/src/layout/AppHeader.tsx index 1f79b199..eb513c73 100644 --- a/frontend/src/layout/AppHeader.tsx +++ b/frontend/src/layout/AppHeader.tsx @@ -7,6 +7,25 @@ import NotificationDropdown from "../components/header/NotificationDropdown"; import UserDropdown from "../components/header/UserDropdown"; import { HeaderMetrics } from "../components/header/HeaderMetrics"; 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 ( + + ); +}; const AppHeader: React.FC = () => { const [isApplicationMenuOpen, setApplicationMenuOpen] = useState(false); @@ -161,6 +180,8 @@ const AppHeader: React.FC = () => {
{/* */} + {/* */} + {/* */} {/* */} diff --git a/frontend/src/pages/Dashboard/Home.tsx b/frontend/src/pages/Dashboard/Home.tsx index 5903074e..1b4b9353 100644 --- a/frontend/src/pages/Dashboard/Home.tsx +++ b/frontend/src/pages/Dashboard/Home.tsx @@ -6,6 +6,8 @@ import UsageChartWidget from "../../components/dashboard/UsageChartWidget"; import EnhancedMetricCard from "../../components/dashboard/EnhancedMetricCard"; import ComponentCard from "../../components/common/ComponentCard"; import PageHeader from "../../components/common/PageHeader"; +import WorkflowGuide from "../../components/onboarding/WorkflowGuide"; +import { useOnboardingStore } from "../../store/onboardingStore"; import { Card } from "../../components/ui/card"; import { ProgressBar } from "../../components/ui/progress"; import { ApexOptions } from "apexcharts"; @@ -115,11 +117,19 @@ export default function Home() { const toast = useToast(); const { activeSite } = useSiteStore(); const { activeSector } = useSectorStore(); + const { isGuideDismissed, showGuide } = useOnboardingStore(); const [insights, setInsights] = useState(null); const [loading, setLoading] = useState(true); const [lastUpdated, setLastUpdated] = useState(new Date()); + // Show guide on first visit if not dismissed + useEffect(() => { + if (!isGuideDismissed) { + showGuide(); + } + }, [isGuideDismissed, showGuide]); + const appModules = [ { title: "Planner", @@ -323,6 +333,9 @@ export default function Home() { onRefresh={fetchAppInsights} /> + {/* Welcome/Guide Screen - Inline at top */} + +
{/* Hero Section */}
diff --git a/frontend/src/pages/Sites/List.tsx b/frontend/src/pages/Sites/List.tsx index 3db5916f..2f71ea43 100644 --- a/frontend/src/pages/Sites/List.tsx +++ b/frontend/src/pages/Sites/List.tsx @@ -1,22 +1,20 @@ /** * Site List View - * Phase 7: UI Components & Prompt Management - * Advanced site list with filters and search + * Refactored to use TablePageTemplate with table view as default + * 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 PageMeta from '../../components/common/PageMeta'; import PageHeader from '../../components/common/PageHeader'; +import TablePageTemplate from '../../templates/TablePageTemplate'; import { Card } from '../../components/ui/card'; 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 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 { PlusIcon, PencilIcon, @@ -25,8 +23,8 @@ import { GridIcon, PlugInIcon, FileIcon, - MoreDotIcon, - PageIcon + PageIcon, + TableIcon } from '../../icons'; import { fetchSites, @@ -39,7 +37,10 @@ import { fetchSiteSectors, Site as SiteType, Industry, + fetchAPI, } from '../../services/api'; +import { useToast } from '../../components/ui/toast/ToastContainer'; +import SiteTypeBadge from '../../components/sites/SiteTypeBadge'; interface Site extends SiteType { page_count?: number; @@ -51,18 +52,20 @@ interface Site extends SiteType { active_sectors_count?: number; } +type ViewType = 'table' | 'grid'; + export default function SiteList() { const navigate = useNavigate(); const toast = useToast(); const [sites, setSites] = useState([]); const [filteredSites, setFilteredSites] = useState([]); const [loading, setLoading] = useState(true); + const [viewType, setViewType] = useState('table'); // Site Management Modals const [selectedSite, setSelectedSite] = useState(null); const [showSiteModal, setShowSiteModal] = useState(false); const [showSectorsModal, setShowSectorsModal] = useState(false); - const [showDetailsModal, setShowDetailsModal] = useState(false); const [isSaving, setIsSaving] = useState(false); const [togglingSiteId, setTogglingSiteId] = useState(null); const [industries, setIndustries] = useState([]); @@ -168,7 +171,7 @@ export default function SiteList() { } } - // Integration filter (has integrations or not) + // Integration filter if (integrationFilter === 'has_integrations') { filtered = filtered.filter((site) => (site.integration_count || 0) > 0); } else if (integrationFilter === 'no_integrations') { @@ -189,18 +192,7 @@ export default function SiteList() { setShowSiteModal(true); }; - const handleIntegration = (siteId: number) => { - 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) => { + const handleEdit = (site: Site) => { setSelectedSite(site); setFormData({ name: site.name || '', @@ -211,13 +203,10 @@ export default function SiteList() { setShowSiteModal(true); }; - const handleSettings = (siteId: number) => { - const site = sites.find(s => s.id === siteId); - if (site) { - setSelectedSite(site); - setShowSectorsModal(true); - loadSiteSectors(site); - } + const handleSettings = (site: Site) => { + setSelectedSite(site); + setShowSectorsModal(true); + loadSiteSectors(site); }; 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 () => { try { 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 () => { if (!selectedSite || !selectedIndustry || selectedSectors.length === 0) { toast.error('Please select an industry and at least one sector'); @@ -373,19 +331,11 @@ export default function SiteList() { } }; - const handleDeleteSite = async (site: Site) => { - if (!window.confirm(`Are you sure you want to delete "${site.name}"? This action cannot be undone.`)) { - return; - } - + const handleDeleteSite = async (siteId: number) => { try { - await deleteSite(site.id); + await deleteSite(siteId); toast.success('Site deleted successfully'); await loadSites(); - if (showDetailsModal) { - setShowDetailsModal(false); - setSelectedSite(null); - } } catch (error: any) { toast.error(`Failed to delete site: ${error.message}`); } @@ -440,24 +390,6 @@ export default function SiteList() { 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 = () => { setSearchTerm(''); setSiteTypeFilter(''); @@ -487,9 +419,6 @@ export default function SiteList() { { value: '', label: 'All Status' }, { value: 'active', label: 'Active' }, { value: 'inactive', label: 'Inactive' }, - { value: 'active', label: 'Active Status' }, - { value: 'inactive', label: 'Inactive Status' }, - { value: 'suspended', label: 'Suspended' }, ]; const INTEGRATION_OPTIONS = [ @@ -498,6 +427,197 @@ export default function SiteList() { { value: 'no_integrations', label: 'No Integrations' }, ]; + // Table columns configuration + const tableColumns = useMemo(() => [ + { + key: 'name', + label: 'Site Name', + sortable: true, + render: (value: string, row: Site) => ( +
+
+ +
+
+
{row.name}
+ {row.domain && ( +
{row.domain}
+ )} +
+
+ ), + }, + { + key: 'hosting_type', + label: 'Hosting', + sortable: true, + render: (value: string, row: Site) => , + }, + { + key: 'industry_name', + label: 'Industry', + sortable: false, + render: (value: string, row: Site) => ( + row.industry_name ? ( + + {row.industry_name} + + ) : ( + - + ) + ), + }, + { + key: 'active_sectors_count', + label: 'Sectors', + sortable: false, + render: (value: number, row: Site) => ( + + {row.active_sectors_count || 0} / 5 + + ), + }, + { + key: 'integration_count', + label: 'Integrations', + sortable: false, + render: (value: number, row: Site) => ( + row.integration_count && row.integration_count > 0 ? ( + + {row.integration_count} + + ) : ( + - + ) + ), + }, + { + key: 'is_active', + label: 'Status', + sortable: true, + render: (value: boolean, row: Site) => ( +
+ + {row.is_active ? 'Active' : 'Inactive'} + +
+ ), + }, + ], []); + + // Grid view component + const renderGridView = () => ( +
+ {filteredSites.map((site) => ( + +
+
+ +
+

+ {site.name} +

+

+ {site.description || 'No description'} +

+ {site.domain && ( +

+ {site.domain} +

+ )} +
+ + {site.industry_name && ( + + {site.industry_name} + + )} + + {site.active_sectors_count || 0} / 5 Sectors + + {site.integration_count && site.integration_count > 0 && ( + + {site.integration_count} integration{site.integration_count > 1 ? 's' : ''} + + )} +
+
+ + {site.is_active ? 'Active' : 'Inactive'} + +
+
+
+
+ + + +
+
+
+ + +
+ handleToggle(site.id, enabled)} + disabled={togglingSiteId === site.id} + /> +
+
+
+ ))} +
+ ); + const hasActiveFilters = searchTerm || siteTypeFilter || hostingTypeFilter || statusFilter || integrationFilter; if (loading) { @@ -517,26 +637,11 @@ export default function SiteList() { , color: 'blue' }} + hideSiteSector={true} /> + {/* Info Alert */}
-
-

- Manage your sites, configure industries, and select sectors. Multiple sites can be active simultaneously. -

-
- - -
-
- - {/* Info Alert */}
- {/* Filters */} - -
-
- -
-

- Filters -

- {hasActiveFilters && ( - + +
+ - )} -
- -
- {/* Search */} -
- -
-
- -
- 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" - /> -
-
- - {/* Site Type */} -
- - setSiteTypeFilter(value)} - /> -
- - {/* Hosting Type */} -
- - setHostingTypeFilter(value)} - /> -
- - {/* Status */} -
- - setStatusFilter(value)} - /> + + Table + +
- -
- - setIntegrationFilter(value)} - /> -
- - - {/* Results Count */} -
- Showing {filteredSites.length} of {sites.length} sites - {hasActiveFilters && ' (filtered)'}
- {/* Sites List */} - {filteredSites.length === 0 ? ( - -

- {hasActiveFilters ? 'No sites match your filters' : 'No sites created yet'} -

- {hasActiveFilters ? ( - - ) : ( - - )} -
+ {/* Table View */} + {viewType === 'table' ? ( + { + 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} + /> ) : ( -
- {filteredSites.map((site) => ( - -
-
- -
-

- {site.name} -

-

- {site.description || 'No description'} -

- {site.domain && ( -

- {site.domain} -

- )} -
- - {site.industry_name && ( - - {site.industry_name} - - )} - - {site.active_sectors_count || 0} / 5 Sectors - - {site.integration_count && site.integration_count > 0 && ( - - {site.integration_count} integration{site.integration_count > 1 ? 's' : ''} - - )} -
- {/* Status Text and Circle - Same row */} -
- - {site.is_active ? 'Active' : 'Inactive'} - -
-
+ <> + {/* Filters for Grid View */} + +
+
+ + 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" + />
-
- {/* Quick Actions */} -
- - - -
- - {/* Secondary Actions */} -
-
- - -
- handleToggle(site.id, enabled)} - disabled={togglingSiteId === site.id} - /> -
+
+ +
+
+ + +
+
+ + +
+
+ {hasActiveFilters && ( +
+ +
+ )} + + + {/* Results Count */} +
+ Showing {filteredSites.length} of {sites.length} sites + {hasActiveFilters && ' (filtered)'} +
+ + {/* Grid View */} + {filteredSites.length === 0 ? ( + +

+ {hasActiveFilters ? 'No sites match your filters' : 'No sites created yet'} +

+ {hasActiveFilters ? ( + + ) : ( + + )}
- ))} -
+ ) : ( + renderGridView() + )} + )} {/* Create/Edit Site Modal */} @@ -896,57 +970,6 @@ export default function SiteList() {
} /> - - {/* Site Details Modal - Editable */} - {selectedSite && ( - { - setShowDetailsModal(false); - setSelectedSite(null); - }} - onSubmit={handleSaveDetails} - title={`Edit Site: ${selectedSite.name}`} - submitLabel="Save Changes" - fields={getSiteFormFields()} - isLoading={isSaving} - customFooter={ -
- -
- - -
-
- } - /> - )}
); } - diff --git a/frontend/src/store/onboardingStore.ts b/frontend/src/store/onboardingStore.ts new file mode 100644 index 00000000..14f10c70 --- /dev/null +++ b/frontend/src/store/onboardingStore.ts @@ -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()( + persist( + (set) => ({ + isGuideDismissed: false, + isGuideVisible: false, + + dismissGuide: () => set({ isGuideDismissed: true, isGuideVisible: false }), + showGuide: () => set({ isGuideVisible: true }), + toggleGuide: () => set((state) => ({ isGuideVisible: !state.isGuideVisible })), + }), + { + name: 'onboarding-storage', + } + ) +); +