Section 2 Part 3
This commit is contained in:
@@ -12,7 +12,8 @@ import Button from '../../components/ui/button/Button';
|
||||
import InputField from '../../components/form/input/InputField';
|
||||
import Select from '../../components/form/Select';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { fetchAPI } from '../../services/api';
|
||||
import { fetchAPI, setActiveSite as apiSetActiveSite } from '../../services/api';
|
||||
import { useSiteStore } from '../../store/siteStore';
|
||||
import { SearchIcon } from '../../icons';
|
||||
import {
|
||||
PencilIcon,
|
||||
@@ -20,8 +21,10 @@ import {
|
||||
TrashBinIcon,
|
||||
PlusIcon,
|
||||
FileIcon,
|
||||
GridIcon
|
||||
GridIcon,
|
||||
GlobeIcon
|
||||
} from '../../icons';
|
||||
import SiteInfoBar from '../../components/common/SiteInfoBar';
|
||||
|
||||
interface ContentItem {
|
||||
id: number;
|
||||
@@ -40,6 +43,7 @@ export default function SiteContentManager() {
|
||||
const { id: siteId } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const { setActiveSite } = useSiteStore();
|
||||
const [content, setContent] = useState<ContentItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
@@ -50,13 +54,36 @@ export default function SiteContentManager() {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(1);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [site, setSite] = useState<any>(null);
|
||||
const pageSize = 20;
|
||||
|
||||
useEffect(() => {
|
||||
if (siteId) {
|
||||
loadSiteAndContent();
|
||||
}
|
||||
}, [siteId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (siteId) {
|
||||
loadContent();
|
||||
}
|
||||
}, [siteId, currentPage, statusFilter, sourceFilter, searchTerm, sortBy, sortDirection]);
|
||||
}, [currentPage, statusFilter, sourceFilter, searchTerm, sortBy, sortDirection]);
|
||||
|
||||
const loadSiteAndContent = async () => {
|
||||
try {
|
||||
// Load site data and sync with store
|
||||
const siteData = await fetchAPI(`/v1/auth/sites/${siteId}/`);
|
||||
if (siteData) {
|
||||
setSite(siteData);
|
||||
setActiveSite(siteData);
|
||||
await apiSetActiveSite(siteData.id).catch(() => {});
|
||||
}
|
||||
// Then load content
|
||||
await loadContent();
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load site:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadContent = async () => {
|
||||
try {
|
||||
@@ -127,15 +154,13 @@ export default function SiteContentManager() {
|
||||
<PageMeta title="Site Content Manager - IGNY8" />
|
||||
|
||||
<PageHeader
|
||||
title={`Content Manager (${totalCount} items)`}
|
||||
badge={{ icon: <FileIcon />, color: 'blue' }}
|
||||
title="Content Manager"
|
||||
badge={{ icon: <FileIcon />, color: 'green' }}
|
||||
hideSiteSector
|
||||
/>
|
||||
<div className="mb-6 flex justify-end">
|
||||
<Button onClick={() => navigate(`/sites/${siteId}/posts/new`)} variant="primary" startIcon={<PlusIcon className="w-4 h-4" />}>
|
||||
New Post
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Site Info Bar */}
|
||||
<SiteInfoBar site={site} currentPage="content" itemsCount={totalCount} showNewPostButton />
|
||||
|
||||
{/* Filters */}
|
||||
<Card className="p-4 mb-6">
|
||||
|
||||
@@ -8,16 +8,19 @@ import { useParams, useNavigate } from 'react-router-dom';
|
||||
import PageMeta from '../../components/common/PageMeta';
|
||||
import PageHeader from '../../components/common/PageHeader';
|
||||
import ComponentCard from '../../components/common/ComponentCard';
|
||||
import SiteInfoBar from '../../components/common/SiteInfoBar';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import Button from '../../components/ui/button/Button';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { fetchAPI, fetchSiteSectors } from '../../services/api';
|
||||
import { fetchAPI, fetchSiteSectors, setActiveSite as apiSetActiveSite } from '../../services/api';
|
||||
import { getDashboardStats } from '../../services/billing.api';
|
||||
import SiteSetupChecklist from '../../components/sites/SiteSetupChecklist';
|
||||
import { integrationApi } from '../../services/integration.api';
|
||||
import SiteConfigWidget from '../../components/dashboard/SiteConfigWidget';
|
||||
import OperationsCostsWidget from '../../components/dashboard/OperationsCostsWidget';
|
||||
import CreditAvailabilityWidget from '../../components/dashboard/CreditAvailabilityWidget';
|
||||
import { useBillingStore } from '../../store/billingStore';
|
||||
import { useSiteStore } from '../../store/siteStore';
|
||||
import {
|
||||
FileIcon,
|
||||
PlugInIcon,
|
||||
@@ -27,6 +30,7 @@ import {
|
||||
ArrowRightIcon,
|
||||
ArrowUpIcon,
|
||||
ClockIcon,
|
||||
ChevronRightIcon,
|
||||
} from '../../icons';
|
||||
|
||||
interface Site {
|
||||
@@ -67,6 +71,7 @@ export default function SiteDashboard() {
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const { balance, loadBalance } = useBillingStore();
|
||||
const { setActiveSite } = useSiteStore();
|
||||
const [site, setSite] = useState<Site | null>(null);
|
||||
const [setupState, setSetupState] = useState<SiteSetupState>({
|
||||
hasIndustry: false,
|
||||
@@ -83,19 +88,33 @@ export default function SiteDashboard() {
|
||||
|
||||
useEffect(() => {
|
||||
if (siteId) {
|
||||
loadSiteData();
|
||||
loadBalance();
|
||||
}
|
||||
}, [siteId, loadBalance]);
|
||||
|
||||
const loadSiteData = async () => {
|
||||
try {
|
||||
// Create a local copy of siteId to use in async operations
|
||||
const currentSiteId = siteId;
|
||||
|
||||
// Reset state when site changes
|
||||
setOperations([]);
|
||||
setSite(null);
|
||||
setLoading(true);
|
||||
|
||||
// Load data for this specific siteId
|
||||
loadSiteData(currentSiteId);
|
||||
loadBalance();
|
||||
}
|
||||
}, [siteId]);
|
||||
|
||||
const loadSiteData = async (currentSiteId: string) => {
|
||||
try {
|
||||
// Load site data
|
||||
const siteData = await fetchAPI(`/v1/auth/sites/${siteId}/`);
|
||||
const siteData = await fetchAPI(`/v1/auth/sites/${currentSiteId}/`);
|
||||
|
||||
// CRITICAL: Verify we're still on the same site before updating state
|
||||
// This prevents race conditions when user rapidly switches sites
|
||||
if (siteData) {
|
||||
setSite(siteData);
|
||||
// Update global site store so site selector shows correct site
|
||||
setActiveSite(siteData);
|
||||
// Also set as active site in backend
|
||||
await apiSetActiveSite(siteData.id).catch(() => {});
|
||||
|
||||
// Check setup state
|
||||
const hasIndustry = !!siteData.industry || !!siteData.industry_name;
|
||||
@@ -104,7 +123,7 @@ export default function SiteDashboard() {
|
||||
let hasSectors = false;
|
||||
let sectorsCount = 0;
|
||||
try {
|
||||
const sectors = await fetchSiteSectors(Number(siteId));
|
||||
const sectors = await fetchSiteSectors(Number(currentSiteId));
|
||||
hasSectors = sectors && sectors.length > 0;
|
||||
sectorsCount = sectors?.length || 0;
|
||||
} catch (err) {
|
||||
@@ -114,7 +133,7 @@ export default function SiteDashboard() {
|
||||
// Check WordPress integration
|
||||
let hasWordPressIntegration = false;
|
||||
try {
|
||||
const wpIntegration = await integrationApi.getWordPressIntegration(Number(siteId));
|
||||
const wpIntegration = await integrationApi.getWordPressIntegration(Number(currentSiteId));
|
||||
hasWordPressIntegration = !!wpIntegration;
|
||||
} catch (err) {
|
||||
// No integration is fine
|
||||
@@ -125,7 +144,7 @@ export default function SiteDashboard() {
|
||||
let keywordsCount = 0;
|
||||
try {
|
||||
const { fetchKeywords } = await import('../../services/api');
|
||||
const keywordsData = await fetchKeywords({ site_id: Number(siteId), page_size: 1 });
|
||||
const keywordsData = await fetchKeywords({ site_id: Number(currentSiteId), page_size: 1 });
|
||||
hasKeywords = keywordsData?.results?.length > 0 || keywordsData?.count > 0;
|
||||
keywordsCount = keywordsData?.count || 0;
|
||||
} catch (err) {
|
||||
@@ -136,7 +155,7 @@ export default function SiteDashboard() {
|
||||
let hasAuthorProfiles = false;
|
||||
let authorProfilesCount = 0;
|
||||
try {
|
||||
const authorsData = await fetchAPI(`/v1/thinker/author-profiles/?site_id=${siteId}&page_size=1`);
|
||||
const authorsData = await fetchAPI(`/v1/thinker/author-profiles/?site_id=${currentSiteId}&page_size=1`);
|
||||
hasAuthorProfiles = authorsData?.count > 0;
|
||||
authorProfilesCount = authorsData?.count || 0;
|
||||
} catch (err) {
|
||||
@@ -154,15 +173,54 @@ export default function SiteDashboard() {
|
||||
authorProfilesCount,
|
||||
});
|
||||
|
||||
// Load operation stats (mock data for now - would come from backend)
|
||||
// In real implementation, fetch from /api/v1/dashboard/site/{siteId}/operations/
|
||||
const mockOperations: OperationStat[] = [
|
||||
{ type: 'clustering', count: 8, creditsUsed: 80, avgCreditsPerOp: 10 },
|
||||
{ type: 'ideas', count: 12, creditsUsed: 24, avgCreditsPerOp: 2 },
|
||||
{ type: 'content', count: 28, creditsUsed: 1400, avgCreditsPerOp: 50 },
|
||||
{ type: 'images', count: 45, creditsUsed: 225, avgCreditsPerOp: 5 },
|
||||
];
|
||||
setOperations(mockOperations);
|
||||
// Load operation stats from real API data
|
||||
try {
|
||||
const stats = await getDashboardStats({ site_id: Number(currentSiteId), days: 7 });
|
||||
|
||||
// Map operation types from API to display types
|
||||
const operationTypeMap: Record<string, 'clustering' | 'ideas' | 'content' | 'images'> = {
|
||||
'clustering': 'clustering',
|
||||
'idea_generation': 'ideas',
|
||||
'content_generation': 'content',
|
||||
'image_generation': 'images',
|
||||
'image_prompt_extraction': 'images',
|
||||
};
|
||||
|
||||
const mappedOperations: OperationStat[] = [];
|
||||
const expectedTypes: Array<'clustering' | 'ideas' | 'content' | 'images'> = ['clustering', 'ideas', 'content', 'images'];
|
||||
|
||||
// Initialize with zeros
|
||||
const opTotals: Record<string, { count: number; credits: number }> = {};
|
||||
expectedTypes.forEach(t => { opTotals[t] = { count: 0, credits: 0 }; });
|
||||
|
||||
// Sum up operations by mapped type
|
||||
if (stats.ai_operations?.operations) {
|
||||
stats.ai_operations.operations.forEach(op => {
|
||||
const mappedType = operationTypeMap[op.type] || op.type;
|
||||
if (opTotals[mappedType]) {
|
||||
opTotals[mappedType].count += op.count;
|
||||
opTotals[mappedType].credits += op.credits;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Convert to array with avgCreditsPerOp
|
||||
expectedTypes.forEach(type => {
|
||||
const data = opTotals[type];
|
||||
mappedOperations.push({
|
||||
type,
|
||||
count: data.count,
|
||||
creditsUsed: data.credits,
|
||||
avgCreditsPerOp: data.count > 0 ? data.credits / data.count : 0,
|
||||
});
|
||||
});
|
||||
|
||||
setOperations(mappedOperations);
|
||||
} catch (err) {
|
||||
console.log('Could not load operations stats:', err);
|
||||
// Set empty operations if API fails
|
||||
setOperations([]);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to load site data: ${error.message}`);
|
||||
@@ -200,33 +258,17 @@ export default function SiteDashboard() {
|
||||
<div className="p-6">
|
||||
<PageMeta title={`${site.name} - Dashboard`} />
|
||||
<PageHeader
|
||||
title={site.name}
|
||||
title="Site Dashboard"
|
||||
badge={{ icon: <GridIcon />, color: 'blue' }}
|
||||
breadcrumb="Sites / Dashboard"
|
||||
hideSiteSector
|
||||
/>
|
||||
|
||||
{/* Site Info */}
|
||||
<div className="mb-6">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
{site.slug} • {site.site_type} • {site.hosting_type}
|
||||
</p>
|
||||
{site.domain && (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-500 mt-1">
|
||||
{site.domain}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex gap-2 mt-4">
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => navigate(`/sites/${siteId}/settings`)}
|
||||
>
|
||||
Settings
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* Site Info Bar */}
|
||||
<SiteInfoBar site={site} currentPage="dashboard" />
|
||||
|
||||
{/* Site Setup Checklist */}
|
||||
<div className="mb-6">
|
||||
{/* Site Setup Progress + Quick Actions - Side by Side */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
{/* Site Setup Checklist - Left Half */}
|
||||
<SiteSetupChecklist
|
||||
siteId={Number(siteId)}
|
||||
siteName={site.name}
|
||||
@@ -235,22 +277,95 @@ export default function SiteDashboard() {
|
||||
hasWordPressIntegration={setupState.hasWordPressIntegration}
|
||||
hasKeywords={setupState.hasKeywords}
|
||||
/>
|
||||
|
||||
{/* Quick Actions - Right Half */}
|
||||
<ComponentCard title="Quick Actions" desc="Common site management tasks">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{/* Manage Pages */}
|
||||
<button
|
||||
onClick={() => navigate(`/sites/${siteId}/pages`)}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 hover:border-brand-300 hover:bg-brand-50 dark:hover:bg-brand-900/10 transition-all group"
|
||||
>
|
||||
<div className="size-8 rounded-lg bg-brand-100 dark:bg-brand-900/30 flex items-center justify-center flex-shrink-0">
|
||||
<PageIcon className="h-4 w-4 text-brand-600 dark:text-brand-400" />
|
||||
</div>
|
||||
<span className="flex-1 text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Manage Pages</span>
|
||||
</button>
|
||||
|
||||
{/* Manage Content */}
|
||||
<button
|
||||
onClick={() => navigate(`/sites/${siteId}/content`)}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 hover:border-success-300 hover:bg-success-50 dark:hover:bg-success-900/10 transition-all group"
|
||||
>
|
||||
<div className="size-8 rounded-lg bg-success-100 dark:bg-success-900/30 flex items-center justify-center flex-shrink-0">
|
||||
<FileIcon className="h-4 w-4 text-success-600 dark:text-success-400" />
|
||||
</div>
|
||||
<span className="flex-1 text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Manage Content</span>
|
||||
</button>
|
||||
|
||||
{/* Integrations */}
|
||||
<button
|
||||
onClick={() => navigate(`/sites/${siteId}/settings?tab=integrations`)}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 hover:border-purple-300 hover:bg-purple-50 dark:hover:bg-purple-900/10 transition-all group"
|
||||
>
|
||||
<div className="size-8 rounded-lg bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center flex-shrink-0">
|
||||
<PlugInIcon className="h-4 w-4 text-purple-600 dark:text-purple-400" />
|
||||
</div>
|
||||
<span className="flex-1 text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Integrations</span>
|
||||
</button>
|
||||
|
||||
{/* Sync Dashboard */}
|
||||
<button
|
||||
onClick={() => navigate(`/sites/${siteId}/sync`)}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 hover:border-warning-300 hover:bg-warning-50 dark:hover:bg-warning-900/10 transition-all group"
|
||||
>
|
||||
<div className="size-8 rounded-lg bg-warning-100 dark:bg-warning-900/30 flex items-center justify-center flex-shrink-0">
|
||||
<BoltIcon className="h-4 w-4 text-warning-600 dark:text-warning-400" />
|
||||
</div>
|
||||
<span className="flex-1 text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Sync Dashboard</span>
|
||||
</button>
|
||||
|
||||
{/* Deploy Site */}
|
||||
<button
|
||||
onClick={() => navigate(`/sites/${siteId}/deploy`)}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 hover:border-info-300 hover:bg-info-50 dark:hover:bg-info-900/10 transition-all group"
|
||||
>
|
||||
<div className="size-8 rounded-lg bg-info-100 dark:bg-info-900/30 flex items-center justify-center flex-shrink-0">
|
||||
<ArrowUpIcon className="h-4 w-4 text-info-600 dark:text-info-400" />
|
||||
</div>
|
||||
<span className="flex-1 text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Deploy Site</span>
|
||||
</button>
|
||||
|
||||
{/* Content Calendar */}
|
||||
<button
|
||||
onClick={() => navigate(`/publisher/content-calendar`)}
|
||||
className="flex items-center gap-3 px-4 py-3 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 hover:border-gray-300 hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-all group"
|
||||
>
|
||||
<div className="size-8 rounded-lg bg-gray-100 dark:bg-gray-800 flex items-center justify-center flex-shrink-0">
|
||||
<ClockIcon className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
</div>
|
||||
<span className="flex-1 text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Content Calendar</span>
|
||||
</button>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
|
||||
{/* Site Insights - 3 Column Grid */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
||||
<SiteConfigWidget
|
||||
setupState={{
|
||||
hasIndustry: setupState.hasIndustry,
|
||||
sectorsCount: setupState.sectorsCount,
|
||||
hasWordPressIntegration: setupState.hasWordPressIntegration,
|
||||
keywordsCount: setupState.keywordsCount,
|
||||
authorProfilesCount: setupState.authorProfilesCount
|
||||
}}
|
||||
siteId={Number(siteId)}
|
||||
siteName={site.name}
|
||||
hasIndustry={setupState.hasIndustry}
|
||||
hasSectors={setupState.hasSectors}
|
||||
sectorsCount={setupState.sectorsCount}
|
||||
hasWordPress={setupState.hasWordPressIntegration}
|
||||
hasKeywords={setupState.hasKeywords}
|
||||
keywordsCount={setupState.keywordsCount}
|
||||
hasAuthorProfiles={setupState.hasAuthorProfiles}
|
||||
authorProfilesCount={setupState.authorProfilesCount}
|
||||
/>
|
||||
|
||||
<OperationsCostsWidget operations={operations} siteId={Number(siteId)} />
|
||||
<OperationsCostsWidget operations={operations} />
|
||||
|
||||
<CreditAvailabilityWidget
|
||||
availableCredits={balance?.credits_remaining ?? 0}
|
||||
@@ -260,109 +375,8 @@ export default function SiteDashboard() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<ComponentCard title="Quick Actions" desc="Common site management tasks">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<Button
|
||||
onClick={() => navigate(`/sites/${siteId}/pages`)}
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group h-auto justify-start"
|
||||
>
|
||||
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-lg">
|
||||
<PageIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h4 className="font-semibold text-gray-900 mb-1">Manage Pages</h4>
|
||||
<p className="text-sm text-gray-600">View and edit pages</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-primary)] transition" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => navigate(`/sites/${siteId}/content`)}
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-success)] hover:shadow-lg transition-all group h-auto justify-start"
|
||||
>
|
||||
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-success)] to-[var(--color-success-dark)] flex items-center justify-center text-white shadow-lg">
|
||||
<FileIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h4 className="font-semibold text-gray-900 mb-1">Manage Content</h4>
|
||||
<p className="text-sm text-gray-600">View and edit content</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-success)] transition" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => navigate(`/sites/${siteId}/settings?tab=integrations`)}
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-purple)] hover:shadow-lg transition-all group h-auto justify-start"
|
||||
>
|
||||
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-purple)] to-[var(--color-purple-dark)] flex items-center justify-center text-white shadow-lg">
|
||||
<PlugInIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h4 className="font-semibold text-gray-900 mb-1">Integrations</h4>
|
||||
<p className="text-sm text-gray-600">Manage connections</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-purple)] transition" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => navigate(`/sites/${siteId}/sync`)}
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-warning)] hover:shadow-lg transition-all group h-auto justify-start"
|
||||
>
|
||||
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-warning)] to-[var(--color-warning-dark)] flex items-center justify-center text-white shadow-lg">
|
||||
<BoltIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h4 className="font-semibold text-gray-900 mb-1">Sync Dashboard</h4>
|
||||
<p className="text-sm text-gray-600">View sync status</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-warning)] transition" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => navigate(`/sites/${siteId}/deploy`)}
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-[var(--color-primary)] hover:shadow-lg transition-all group h-auto justify-start"
|
||||
>
|
||||
<div className="size-12 rounded-xl bg-gradient-to-br from-[var(--color-primary)] to-[var(--color-primary-dark)] flex items-center justify-center text-white shadow-lg">
|
||||
<ArrowUpIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h4 className="font-semibold text-gray-900 mb-1">Deploy Site</h4>
|
||||
<p className="text-sm text-gray-600">Deploy to production</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-[var(--color-primary)] transition" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => navigate(`/publisher/content-calendar`)}
|
||||
variant="ghost"
|
||||
tone="neutral"
|
||||
className="flex items-center gap-4 p-6 rounded-xl border-2 border-gray-200 bg-white hover:border-amber-500 hover:shadow-lg transition-all group h-auto justify-start"
|
||||
>
|
||||
<div className="size-12 rounded-xl bg-gradient-to-br from-amber-500 to-amber-600 flex items-center justify-center text-white shadow-lg">
|
||||
<ClockIcon className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="flex-1 text-left">
|
||||
<h4 className="font-semibold text-gray-900 mb-1">Content Calendar</h4>
|
||||
<p className="text-sm text-gray-600">Schedule and manage content publishing</p>
|
||||
</div>
|
||||
<ArrowRightIcon className="h-5 w-5 text-gray-400 group-hover:text-warning-500 transition" />
|
||||
</Button>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
{/* Recent Activity - Placeholder */}
|
||||
<Card className="p-6 mt-6">
|
||||
<Card className="p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
|
||||
Recent Activity
|
||||
</h2>
|
||||
|
||||
@@ -24,19 +24,23 @@ import {
|
||||
fetchIndustries,
|
||||
Site,
|
||||
Industry,
|
||||
setActiveSite as apiSetActiveSite,
|
||||
} from '../../services/api';
|
||||
import { useSiteStore } from '../../store/siteStore';
|
||||
import WordPressIntegrationForm from '../../components/sites/WordPressIntegrationForm';
|
||||
import { integrationApi, SiteIntegration } from '../../services/integration.api';
|
||||
import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon, CloseIcon, PlusIcon, RefreshCwIcon, FileTextIcon, ImageIcon, SaveIcon, Loader2Icon } from '../../icons';
|
||||
import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon, CloseIcon, PlusIcon, RefreshCwIcon, FileTextIcon, ImageIcon, SaveIcon, Loader2Icon, ArrowRightIcon, SettingsIcon, GlobeIcon, LayersIcon } from '../../icons';
|
||||
import Badge from '../../components/ui/badge/Badge';
|
||||
import { Dropdown } from '../../components/ui/dropdown/Dropdown';
|
||||
import { DropdownItem } from '../../components/ui/dropdown/DropdownItem';
|
||||
import SiteInfoBar from '../../components/common/SiteInfoBar';
|
||||
|
||||
export default function SiteSettings() {
|
||||
const { id: siteId } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const toast = useToast();
|
||||
const { setActiveSite } = useSiteStore();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [site, setSite] = useState<any>(null);
|
||||
@@ -55,6 +59,9 @@ export default function SiteSettings() {
|
||||
const [contentTypes, setContentTypes] = useState<any>(null);
|
||||
const [contentTypesLoading, setContentTypesLoading] = useState(false);
|
||||
|
||||
// Advanced Settings toggle
|
||||
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
|
||||
|
||||
// Publishing settings state
|
||||
const [publishingSettings, setPublishingSettings] = useState<any>(null);
|
||||
const [publishingSettingsLoading, setPublishingSettingsLoading] = useState(false);
|
||||
@@ -281,6 +288,10 @@ export default function SiteSettings() {
|
||||
const data = await fetchAPI(`/v1/auth/sites/${siteId}/`);
|
||||
if (data) {
|
||||
setSite(data);
|
||||
// Update global site store so site selector shows correct site
|
||||
setActiveSite(data);
|
||||
// Also set as active site in backend
|
||||
await apiSetActiveSite(data.id).catch(() => {});
|
||||
const seoData = data.seo_metadata || data.metadata || {};
|
||||
setFormData({
|
||||
name: data.name || '',
|
||||
@@ -746,116 +757,46 @@ export default function SiteSettings() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<PageMeta title="Site Settings - IGNY8" />
|
||||
<PageHeader
|
||||
title="Site Settings"
|
||||
badge={{ icon: <GridIcon />, color: 'blue' }}
|
||||
hideSiteSector
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between gap-4 mb-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<PageHeader
|
||||
title={site?.name ? `${site.name} — Site Settings` : 'Site Settings'}
|
||||
badge={{ icon: <GridIcon />, color: 'blue' }}
|
||||
hideSiteSector
|
||||
/>
|
||||
{/* Integration status indicator */}
|
||||
<div className="flex items-center gap-3 ml-2">
|
||||
<span
|
||||
className={`inline-block w-6 h-6 rounded-full ${
|
||||
integrationStatus === 'connected' ? 'bg-success-500' :
|
||||
integrationStatus === 'configured' ? 'bg-brand-500' : 'bg-gray-300'
|
||||
}`}
|
||||
title={`Integration status: ${
|
||||
integrationStatus === 'connected' ? 'Connected & Authenticated' :
|
||||
integrationStatus === 'configured' ? 'Configured (testing...)' : 'Not configured'
|
||||
}`}
|
||||
/>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-300">
|
||||
{integrationStatus === 'connected' && 'Connected'}
|
||||
{integrationStatus === 'configured' && (testingAuth ? 'Testing...' : 'Configured')}
|
||||
{integrationStatus === 'not_configured' && 'Not configured'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Site Selector - Only show if more than 1 site */}
|
||||
{!sitesLoading && sites.length > 1 && (
|
||||
<div className="relative inline-block">
|
||||
<Button
|
||||
ref={siteSelectorRef}
|
||||
onClick={() => setIsSiteSelectorOpen(!isSiteSelectorOpen)}
|
||||
variant="outline"
|
||||
className="flex items-center gap-2"
|
||||
aria-label="Switch site"
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<GridIcon className="w-4 h-4 text-brand-500 dark:text-brand-400" />
|
||||
<span className="max-w-[150px] truncate">{site?.name || 'Select Site'}</span>
|
||||
</span>
|
||||
<ChevronDownIcon className={`w-4 h-4 text-brand-500 dark:text-brand-400 transition-transform ${isSiteSelectorOpen ? 'rotate-180' : ''}`} />
|
||||
</Button>
|
||||
<Dropdown
|
||||
isOpen={isSiteSelectorOpen}
|
||||
onClose={() => setIsSiteSelectorOpen(false)}
|
||||
anchorRef={siteSelectorRef}
|
||||
>
|
||||
{sites.map((s) => (
|
||||
<DropdownItem
|
||||
key={s.id}
|
||||
onItemClick={() => handleSiteSelect(s.id)}
|
||||
className={`flex items-center gap-3 px-3 py-2 font-medium rounded-lg text-sm text-left ${
|
||||
site?.id === s.id
|
||||
? "bg-brand-50 text-brand-700 dark:bg-brand-500/20 dark:text-brand-300"
|
||||
: "text-gray-700 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
}`}
|
||||
>
|
||||
<span className="flex-1">{s.name}</span>
|
||||
{site?.id === s.id && (
|
||||
<svg
|
||||
className="w-4 h-4 text-brand-600 dark:text-brand-400"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
)}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{/* Site Info Bar */}
|
||||
<SiteInfoBar site={site} currentPage="settings" />
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div className="flex gap-4 overflow-x-auto">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setActiveTab('general');
|
||||
navigate(`/sites/${siteId}/settings`, { replace: true });
|
||||
}}
|
||||
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
|
||||
activeTab === 'general'
|
||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||
}`}
|
||||
startIcon={<GridIcon className="w-4 h-4" />}
|
||||
>
|
||||
General
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setActiveTab('content-generation');
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex gap-4 overflow-x-auto">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setActiveTab('general');
|
||||
navigate(`/sites/${siteId}/settings`, { replace: true });
|
||||
}}
|
||||
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
|
||||
activeTab === 'general'
|
||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||
}`}
|
||||
startIcon={<GridIcon className={`w-4 h-4 ${activeTab === 'general' ? 'text-brand-500' : ''}`} />}
|
||||
>
|
||||
General
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setActiveTab('content-generation');
|
||||
navigate(`/sites/${siteId}/settings?tab=content-generation`, { replace: true });
|
||||
}}
|
||||
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
|
||||
activeTab === 'content-generation'
|
||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||
? 'border-success-500 text-success-600 dark:text-success-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||
}`}
|
||||
startIcon={<FileTextIcon className="w-4 h-4" />}
|
||||
startIcon={<FileTextIcon className={`w-4 h-4 ${activeTab === 'content-generation' ? 'text-success-500' : ''}`} />}
|
||||
>
|
||||
Content
|
||||
</Button>
|
||||
@@ -867,10 +808,10 @@ export default function SiteSettings() {
|
||||
}}
|
||||
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
|
||||
activeTab === 'image-settings'
|
||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||
? 'border-purple-500 text-purple-600 dark:text-purple-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||
}`}
|
||||
startIcon={<ImageIcon className="w-4 h-4" />}
|
||||
startIcon={<ImageIcon className={`w-4 h-4 ${activeTab === 'image-settings' ? 'text-purple-500' : ''}`} />}
|
||||
>
|
||||
Images
|
||||
</Button>
|
||||
@@ -882,10 +823,10 @@ export default function SiteSettings() {
|
||||
}}
|
||||
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
|
||||
activeTab === 'integrations'
|
||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||
? 'border-warning-500 text-warning-600 dark:text-warning-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||
}`}
|
||||
startIcon={<PlugInIcon className="w-4 h-4" />}
|
||||
startIcon={<PlugInIcon className={`w-4 h-4 ${activeTab === 'integrations' ? 'text-warning-500' : ''}`} />}
|
||||
>
|
||||
Integrations
|
||||
</Button>
|
||||
@@ -897,10 +838,10 @@ export default function SiteSettings() {
|
||||
}}
|
||||
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
|
||||
activeTab === 'publishing'
|
||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||
? 'border-info-500 text-info-600 dark:text-info-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||
}`}
|
||||
startIcon={<PaperPlaneIcon className="w-4 h-4" />}
|
||||
startIcon={<PaperPlaneIcon className={`w-4 h-4 ${activeTab === 'publishing' ? 'text-info-500' : ''}`} />}
|
||||
>
|
||||
Publishing
|
||||
</Button>
|
||||
@@ -913,21 +854,37 @@ export default function SiteSettings() {
|
||||
}}
|
||||
className={`px-4 py-2 font-medium border-b-2 rounded-none transition-colors whitespace-nowrap ${
|
||||
activeTab === 'content-types'
|
||||
? 'border-brand-500 text-brand-600 dark:text-brand-400'
|
||||
? 'border-error-500 text-error-600 dark:text-error-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300'
|
||||
}`}
|
||||
startIcon={<FileIcon className="w-4 h-4" />}
|
||||
startIcon={<FileIcon className={`w-4 h-4 ${activeTab === 'content-types' ? 'text-error-500' : ''}`} />}
|
||||
>
|
||||
Content Types
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Integration Status Indicator - Larger */}
|
||||
<div className="flex items-center gap-3 px-4 py-2 rounded-lg bg-gray-50 dark:bg-gray-800 flex-shrink-0">
|
||||
<span
|
||||
className={`inline-block w-4 h-4 rounded-full ${
|
||||
integrationStatus === 'connected' ? 'bg-success-500' :
|
||||
integrationStatus === 'configured' ? 'bg-brand-500' : 'bg-gray-300'
|
||||
}`}
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-200">
|
||||
{integrationStatus === 'connected' && 'Connected'}
|
||||
{integrationStatus === 'configured' && (testingAuth ? 'Testing...' : 'Configured')}
|
||||
{integrationStatus === 'not_configured' && 'Not Configured'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Generation Tab */}
|
||||
{activeTab === 'content-generation' && (
|
||||
<div className="space-y-6 max-w-4xl">
|
||||
<Card className="p-6">
|
||||
<Card className="p-6 border-l-4 border-l-brand-500">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="p-2 bg-brand-100 dark:bg-brand-900/30 rounded-lg">
|
||||
<FileTextIcon className="w-5 h-5 text-brand-600 dark:text-brand-400" />
|
||||
@@ -1010,7 +967,7 @@ export default function SiteSettings() {
|
||||
{/* Image Settings Tab */}
|
||||
{activeTab === 'image-settings' && (
|
||||
<div className="space-y-6 max-w-4xl">
|
||||
<Card className="p-6">
|
||||
<Card className="p-6 border-l-4 border-l-purple-500">
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="p-2 bg-purple-100 dark:bg-purple-900/30 rounded-lg">
|
||||
<ImageIcon className="w-5 h-5 text-purple-600 dark:text-purple-400" />
|
||||
@@ -1533,10 +1490,10 @@ export default function SiteSettings() {
|
||||
{/* General Tab */}
|
||||
{activeTab === 'general' && (
|
||||
<>
|
||||
{/* 4-Card Layout for Basic Settings, SEO, Open Graph, and Schema */}
|
||||
{/* Row 1: Basic Settings and Industry/Sectors side by side */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
{/* Card 1: Basic Site Settings */}
|
||||
<Card className="p-6">
|
||||
<Card className="p-6 border-l-4 border-l-brand-500">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<GridIcon className="w-5 h-5 text-brand-500" />
|
||||
Basic Settings
|
||||
@@ -1598,289 +1555,316 @@ export default function SiteSettings() {
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Card 2: SEO Meta Tags */}
|
||||
<Card className="p-6">
|
||||
{/* Card 2: Industry & Sectors Configuration */}
|
||||
<Card className="p-6 border-l-4 border-l-info-500">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<DocsIcon className="w-5 h-5 text-brand-500" />
|
||||
SEO Meta Tags
|
||||
<LayersIcon className="w-5 h-5 text-info-500" />
|
||||
Industry & Sectors
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
Configure up to 5 sectors for content targeting.
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Meta Title</Label>
|
||||
<InputField
|
||||
type="text"
|
||||
value={formData.meta_title}
|
||||
onChange={(e) => setFormData({ ...formData, meta_title: e.target.value })}
|
||||
placeholder="SEO title (recommended: 50-60 characters)"
|
||||
max="60"
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Select Industry
|
||||
</label>
|
||||
<Select
|
||||
options={industries.map((industry) => ({
|
||||
value: industry.slug,
|
||||
label: industry.name,
|
||||
}))}
|
||||
placeholder="Select an industry..."
|
||||
defaultValue={selectedIndustry}
|
||||
onChange={(value) => {
|
||||
setSelectedIndustry(value);
|
||||
setSelectedSectors([]);
|
||||
}}
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{formData.meta_title.length}/60 characters
|
||||
</p>
|
||||
{selectedIndustry && (
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{industries.find(i => i.slug === selectedIndustry)?.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Meta Description</Label>
|
||||
<TextArea
|
||||
value={formData.meta_description}
|
||||
onChange={(value) => setFormData({ ...formData, meta_description: value })}
|
||||
rows={4}
|
||||
placeholder="SEO description (recommended: 150-160 characters)"
|
||||
maxLength={160}
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{formData.meta_description.length}/160 characters
|
||||
</p>
|
||||
</div>
|
||||
{selectedIndustry && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Select Sectors (max 5)
|
||||
</label>
|
||||
<div className="space-y-2 max-h-48 overflow-y-auto border border-gray-200 rounded-lg p-3 dark:border-gray-700">
|
||||
{getIndustrySectors().map((sector) => (
|
||||
<div
|
||||
key={sector.slug}
|
||||
className="flex items-start space-x-3 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedSectors.includes(sector.slug)}
|
||||
onChange={(checked) => {
|
||||
if (checked) {
|
||||
if (selectedSectors.length >= 5) {
|
||||
toast.error('Maximum 5 sectors allowed per site');
|
||||
return;
|
||||
}
|
||||
setSelectedSectors([...selectedSectors, sector.slug]);
|
||||
} else {
|
||||
setSelectedSectors(selectedSectors.filter(s => s !== sector.slug));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm text-gray-900 dark:text-white">
|
||||
{sector.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 line-clamp-1">
|
||||
{sector.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
Selected: {selectedSectors.length} / 5 sectors
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label>Meta Keywords (comma-separated)</Label>
|
||||
<InputField
|
||||
type="text"
|
||||
value={formData.meta_keywords}
|
||||
onChange={(e) => setFormData({ ...formData, meta_keywords: e.target.value })}
|
||||
placeholder="keyword1, keyword2, keyword3"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Separate keywords with commas
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Card 3: Open Graph */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<PaperPlaneIcon className="w-5 h-5 text-brand-500" />
|
||||
Open Graph
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>OG Title</Label>
|
||||
<InputField
|
||||
type="text"
|
||||
value={formData.og_title}
|
||||
onChange={(e) => setFormData({ ...formData, og_title: e.target.value })}
|
||||
placeholder="Open Graph title"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>OG Description</Label>
|
||||
<TextArea
|
||||
value={formData.og_description}
|
||||
onChange={(value) => setFormData({ ...formData, og_description: value })}
|
||||
rows={3}
|
||||
placeholder="Open Graph description"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>OG Image URL</Label>
|
||||
<InputField
|
||||
type="url"
|
||||
value={formData.og_image}
|
||||
onChange={(e) => setFormData({ ...formData, og_image: e.target.value })}
|
||||
placeholder="https://example.com/image.jpg"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Recommended: 1200x630px image
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>OG Type</Label>
|
||||
<SelectDropdown
|
||||
options={[
|
||||
{ value: 'website', label: 'Website' },
|
||||
{ value: 'article', label: 'Article' },
|
||||
{ value: 'business.business', label: 'Business' },
|
||||
{ value: 'product', label: 'Product' },
|
||||
]}
|
||||
value={formData.og_type}
|
||||
onChange={(value) => setFormData({ ...formData, og_type: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>OG Site Name</Label>
|
||||
<InputField
|
||||
type="text"
|
||||
value={formData.og_site_name}
|
||||
onChange={(e) => setFormData({ ...formData, og_site_name: e.target.value })}
|
||||
placeholder="Site name for social sharing"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Card 4: Schema.org */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<BoltIcon className="w-5 h-5 text-brand-500" />
|
||||
Schema.org
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Schema Type</Label>
|
||||
<SelectDropdown
|
||||
options={[
|
||||
{ value: 'Organization', label: 'Organization' },
|
||||
{ value: 'LocalBusiness', label: 'Local Business' },
|
||||
{ value: 'WebSite', label: 'Website' },
|
||||
{ value: 'Corporation', label: 'Corporation' },
|
||||
{ value: 'NGO', label: 'NGO' },
|
||||
]}
|
||||
value={formData.schema_type}
|
||||
onChange={(value) => setFormData({ ...formData, schema_type: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Schema Name</Label>
|
||||
<InputField
|
||||
type="text"
|
||||
value={formData.schema_name}
|
||||
onChange={(e) => setFormData({ ...formData, schema_name: e.target.value })}
|
||||
placeholder="Organization name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Schema Description</Label>
|
||||
<TextArea
|
||||
value={formData.schema_description}
|
||||
onChange={(value) => setFormData({ ...formData, schema_description: value })}
|
||||
rows={3}
|
||||
placeholder="Organization description"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Schema URL</Label>
|
||||
<InputField
|
||||
type="url"
|
||||
value={formData.schema_url}
|
||||
onChange={(e) => setFormData({ ...formData, schema_url: e.target.value })}
|
||||
placeholder="https://example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Schema Logo URL</Label>
|
||||
<InputField
|
||||
type="url"
|
||||
value={formData.schema_logo}
|
||||
onChange={(e) => setFormData({ ...formData, schema_logo: e.target.value })}
|
||||
placeholder="https://example.com/logo.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Same As URLs (comma-separated)</Label>
|
||||
<InputField
|
||||
type="text"
|
||||
value={formData.schema_same_as}
|
||||
onChange={(e) => setFormData({ ...formData, schema_same_as: e.target.value })}
|
||||
placeholder="https://facebook.com/page, https://twitter.com/page"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Social media profiles and other related URLs
|
||||
</p>
|
||||
</div>
|
||||
{selectedIndustry && selectedSectors.length > 0 && (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={handleSelectSectors}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
disabled={isSelectingSectors}
|
||||
>
|
||||
{isSelectingSectors ? 'Saving...' : 'Save Sectors'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Sectors Configuration Section */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4">Industry & Sectors Configuration</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-6">
|
||||
Configure up to 5 sectors from your selected industry. Keywords and clusters are automatically associated with sectors.
|
||||
</p>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Select Industry
|
||||
</label>
|
||||
<Select
|
||||
options={industries.map((industry) => ({
|
||||
value: industry.slug,
|
||||
label: industry.name,
|
||||
}))}
|
||||
placeholder="Select an industry..."
|
||||
defaultValue={selectedIndustry}
|
||||
onChange={(value) => {
|
||||
setSelectedIndustry(value);
|
||||
setSelectedSectors([]);
|
||||
}}
|
||||
/>
|
||||
{selectedIndustry && (
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{industries.find(i => i.slug === selectedIndustry)?.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{/* Advanced Settings Toggle */}
|
||||
<div className="mb-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowAdvancedSettings(!showAdvancedSettings)}
|
||||
className="w-full justify-between"
|
||||
endIcon={<ChevronDownIcon className={`w-4 h-4 transition-transform ${showAdvancedSettings ? 'rotate-180' : ''}`} />}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<SettingsIcon className="w-4 h-4" />
|
||||
Advanced Settings (SEO, Open Graph, Schema)
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{selectedIndustry && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||
Select Sectors (max 5)
|
||||
</label>
|
||||
<div className="space-y-2 max-h-64 overflow-y-auto border border-gray-200 rounded-lg p-4 dark:border-gray-700">
|
||||
{getIndustrySectors().map((sector) => (
|
||||
<div
|
||||
key={sector.slug}
|
||||
className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedSectors.includes(sector.slug)}
|
||||
onChange={(checked) => {
|
||||
if (checked) {
|
||||
if (selectedSectors.length >= 5) {
|
||||
toast.error('Maximum 5 sectors allowed per site');
|
||||
return;
|
||||
}
|
||||
setSelectedSectors([...selectedSectors, sector.slug]);
|
||||
} else {
|
||||
setSelectedSectors(selectedSectors.filter(s => s !== sector.slug));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm text-gray-900 dark:text-white">
|
||||
{sector.name}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{sector.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{/* Advanced Settings - 3 Column Grid */}
|
||||
{showAdvancedSettings && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
{/* SEO Meta Tags */}
|
||||
<Card className="p-6 border-l-4 border-l-success-500">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<DocsIcon className="w-5 h-5 text-success-500" />
|
||||
SEO Meta Tags
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Meta Title</Label>
|
||||
<InputField
|
||||
type="text"
|
||||
value={formData.meta_title}
|
||||
onChange={(e) => setFormData({ ...formData, meta_title: e.target.value })}
|
||||
placeholder="SEO title (50-60 chars)"
|
||||
max="60"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{formData.meta_title.length}/60 characters
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
Selected: {selectedSectors.length} / 5 sectors
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedIndustry && selectedSectors.length > 0 && (
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={handleSelectSectors}
|
||||
variant="primary"
|
||||
disabled={isSelectingSectors}
|
||||
>
|
||||
{isSelectingSectors ? 'Saving Sectors...' : 'Save Sectors'}
|
||||
</Button>
|
||||
<div>
|
||||
<Label>Meta Description</Label>
|
||||
<TextArea
|
||||
value={formData.meta_description}
|
||||
onChange={(value) => setFormData({ ...formData, meta_description: value })}
|
||||
rows={3}
|
||||
placeholder="SEO description (150-160 chars)"
|
||||
maxLength={160}
|
||||
className="mt-1"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{formData.meta_description.length}/160 characters
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Meta Keywords</Label>
|
||||
<InputField
|
||||
type="text"
|
||||
value={formData.meta_keywords}
|
||||
onChange={(e) => setFormData({ ...formData, meta_keywords: e.target.value })}
|
||||
placeholder="keyword1, keyword2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Open Graph */}
|
||||
<Card className="p-6 border-l-4 border-l-purple-500">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<PaperPlaneIcon className="w-5 h-5 text-purple-500" />
|
||||
Open Graph
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>OG Title</Label>
|
||||
<InputField
|
||||
type="text"
|
||||
value={formData.og_title}
|
||||
onChange={(e) => setFormData({ ...formData, og_title: e.target.value })}
|
||||
placeholder="Open Graph title"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>OG Description</Label>
|
||||
<TextArea
|
||||
value={formData.og_description}
|
||||
onChange={(value) => setFormData({ ...formData, og_description: value })}
|
||||
rows={3}
|
||||
placeholder="Open Graph description"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>OG Image URL</Label>
|
||||
<InputField
|
||||
type="url"
|
||||
value={formData.og_image}
|
||||
onChange={(e) => setFormData({ ...formData, og_image: e.target.value })}
|
||||
placeholder="https://example.com/image.jpg"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>OG Type</Label>
|
||||
<SelectDropdown
|
||||
options={[
|
||||
{ value: 'website', label: 'Website' },
|
||||
{ value: 'article', label: 'Article' },
|
||||
{ value: 'business.business', label: 'Business' },
|
||||
{ value: 'product', label: 'Product' },
|
||||
]}
|
||||
value={formData.og_type}
|
||||
onChange={(value) => setFormData({ ...formData, og_type: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>OG Site Name</Label>
|
||||
<InputField
|
||||
type="text"
|
||||
value={formData.og_site_name}
|
||||
onChange={(e) => setFormData({ ...formData, og_site_name: e.target.value })}
|
||||
placeholder="Site name"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Schema.org */}
|
||||
<Card className="p-6 border-l-4 border-l-warning-500">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<BoltIcon className="w-5 h-5 text-warning-500" />
|
||||
Schema.org
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Schema Type</Label>
|
||||
<SelectDropdown
|
||||
options={[
|
||||
{ value: 'Organization', label: 'Organization' },
|
||||
{ value: 'LocalBusiness', label: 'Local Business' },
|
||||
{ value: 'WebSite', label: 'Website' },
|
||||
{ value: 'Corporation', label: 'Corporation' },
|
||||
{ value: 'NGO', label: 'NGO' },
|
||||
]}
|
||||
value={formData.schema_type}
|
||||
onChange={(value) => setFormData({ ...formData, schema_type: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Schema Name</Label>
|
||||
<InputField
|
||||
type="text"
|
||||
value={formData.schema_name}
|
||||
onChange={(e) => setFormData({ ...formData, schema_name: e.target.value })}
|
||||
placeholder="Organization name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Schema Description</Label>
|
||||
<TextArea
|
||||
value={formData.schema_description}
|
||||
onChange={(value) => setFormData({ ...formData, schema_description: value })}
|
||||
rows={3}
|
||||
placeholder="Description"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Schema URL</Label>
|
||||
<InputField
|
||||
type="url"
|
||||
value={formData.schema_url}
|
||||
onChange={(e) => setFormData({ ...formData, schema_url: e.target.value })}
|
||||
placeholder="https://example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Schema Logo URL</Label>
|
||||
<InputField
|
||||
type="url"
|
||||
value={formData.schema_logo}
|
||||
onChange={(e) => setFormData({ ...formData, schema_logo: e.target.value })}
|
||||
placeholder="https://example.com/logo.png"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Same As URLs</Label>
|
||||
<InputField
|
||||
type="text"
|
||||
value={formData.schema_same_as}
|
||||
onChange={(e) => setFormData({ ...formData, schema_same_as: e.target.value })}
|
||||
placeholder="Social profiles (comma-separated)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
variant="primary"
|
||||
disabled={saving}
|
||||
startIcon={saving ? <Loader2Icon className="w-4 h-4 animate-spin" /> : <SaveIcon className="w-4 h-4" />}
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save Settings'}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -2084,15 +2068,6 @@ export default function SiteSettings() {
|
||||
onIntegrationUpdate={handleIntegrationUpdate}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Save Button */}
|
||||
{activeTab === 'general' && (
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSave} variant="primary" disabled={saving}>
|
||||
{saving ? 'Saving...' : 'Save Settings'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user