site settigns
This commit is contained in:
@@ -66,17 +66,8 @@ export default function SiteList() {
|
||||
// Site Management Modals
|
||||
const [selectedSite, setSelectedSite] = useState<Site | null>(null);
|
||||
const [showSiteModal, setShowSiteModal] = useState(false);
|
||||
const [showSectorsModal, setShowSectorsModal] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [togglingSiteId, setTogglingSiteId] = useState<number | null>(null);
|
||||
const [industries, setIndustries] = useState<Industry[]>([]);
|
||||
const [selectedIndustry, setSelectedIndustry] = useState<string>('');
|
||||
const [selectedSectors, setSelectedSectors] = useState<string[]>([]);
|
||||
const [isSelectingSectors, setIsSelectingSectors] = useState(false);
|
||||
const [userPreferences, setUserPreferences] = useState<{
|
||||
selectedIndustry?: string;
|
||||
selectedSectors?: string[];
|
||||
} | null>(null);
|
||||
|
||||
// Form state for site creation/editing
|
||||
const [formData, setFormData] = useState({
|
||||
@@ -95,8 +86,6 @@ export default function SiteList() {
|
||||
|
||||
useEffect(() => {
|
||||
loadSites();
|
||||
loadIndustries();
|
||||
loadUserPreferences();
|
||||
}, []);
|
||||
|
||||
const loadUserPreferences = async () => {
|
||||
@@ -155,33 +144,6 @@ export default function SiteList() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadIndustries = async () => {
|
||||
try {
|
||||
const response = await fetchIndustries();
|
||||
let allIndustries = response.industries || [];
|
||||
|
||||
// Filter to show only user's pre-selected industries/sectors from account preferences
|
||||
try {
|
||||
const { fetchAccountSetting } = await import('../../services/api');
|
||||
const setting = await fetchAccountSetting('user_preferences');
|
||||
const preferences = setting.config as { selectedIndustry?: string; selectedSectors?: string[] } | undefined;
|
||||
|
||||
if (preferences?.selectedIndustry) {
|
||||
// Filter industries to only show the user's pre-selected industry
|
||||
allIndustries = allIndustries.filter(i => i.slug === preferences.selectedIndustry);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// 404 means preferences don't exist yet - show all industries (expected for new users)
|
||||
// 500 and other errors - show all industries (graceful degradation)
|
||||
// Silently handle errors - user can still use the page
|
||||
}
|
||||
|
||||
setIndustries(allIndustries);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load industries:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const applyFilters = () => {
|
||||
let filtered = [...sites];
|
||||
|
||||
@@ -638,15 +600,6 @@ export default function SiteList() {
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleSettings(site)}
|
||||
title="Configure Sectors"
|
||||
>
|
||||
<GridIcon className="w-4 h-4 mr-1" />
|
||||
<span className="text-xs">Sectors</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -927,110 +880,6 @@ export default function SiteList() {
|
||||
fields={getSiteFormFields()}
|
||||
isLoading={isSaving}
|
||||
/>
|
||||
|
||||
{/* Sectors Selection Modal */}
|
||||
<FormModal
|
||||
isOpen={showSectorsModal}
|
||||
onClose={() => setShowSectorsModal(false)}
|
||||
onSubmit={handleSelectSectors}
|
||||
title={selectedSite ? `Configure Sectors for ${selectedSite.name}` : 'Configure Sectors'}
|
||||
submitLabel={isSelectingSectors ? 'Saving...' : 'Save Sectors'}
|
||||
cancelLabel="Cancel"
|
||||
isLoading={isSelectingSectors}
|
||||
className="max-w-2xl"
|
||||
customBody={
|
||||
<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
|
||||
value={selectedIndustry}
|
||||
onChange={(e) => {
|
||||
setSelectedIndustry(e.target.value);
|
||||
setSelectedSectors([]);
|
||||
}}
|
||||
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
>
|
||||
<option value="">Select an industry...</option>
|
||||
{industries.map((industry) => (
|
||||
<option key={industry.slug} value={industry.slug}>
|
||||
{industry.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedIndustry && (
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{industries.find(i => i.slug === selectedIndustry)?.description}
|
||||
</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-64 overflow-y-auto border border-gray-200 rounded-lg p-4 dark:border-gray-700">
|
||||
{getIndustrySectors().map((sector) => (
|
||||
<label
|
||||
key={sector.slug}
|
||||
className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedSectors.includes(sector.slug)}
|
||||
onChange={(e) => {
|
||||
if (e.target.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));
|
||||
}
|
||||
}}
|
||||
className="mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<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>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
Selected: {selectedSectors.length} / 5 sectors
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
customFooter={
|
||||
<div className="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setShowSectorsModal(false)}
|
||||
disabled={isSelectingSectors}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
disabled={!selectedIndustry || selectedSectors.length === 0 || isSelectingSectors}
|
||||
>
|
||||
{isSelectingSectors ? 'Saving...' : 'Save Sectors'}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Site Settings (Advanced)
|
||||
* Phase 7: Advanced Site Management
|
||||
* Features: SEO (meta tags, Open Graph, schema.org)
|
||||
* Features: SEO (meta tags, Open Graph, schema.org), Industry & Sectors Configuration
|
||||
*/
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
@@ -14,7 +14,13 @@ import SelectDropdown from '../../components/form/SelectDropdown';
|
||||
import Checkbox from '../../components/form/input/Checkbox';
|
||||
import TextArea from '../../components/form/input/TextArea';
|
||||
import { useToast } from '../../components/ui/toast/ToastContainer';
|
||||
import { fetchAPI, runSync, fetchSites, Site } from '../../services/api';
|
||||
import {
|
||||
fetchAPI,
|
||||
fetchSites,
|
||||
fetchIndustries,
|
||||
Site,
|
||||
Industry,
|
||||
} from '../../services/api';
|
||||
import WordPressIntegrationForm from '../../components/sites/WordPressIntegrationForm';
|
||||
import { integrationApi, SiteIntegration } from '../../services/integration.api';
|
||||
import { GridIcon, PlugInIcon, PaperPlaneIcon, DocsIcon, BoltIcon, FileIcon, ChevronDownIcon } from '../../icons';
|
||||
@@ -40,10 +46,21 @@ export default function SiteSettings() {
|
||||
const siteSelectorRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
// Check for tab parameter in URL
|
||||
const initialTab = (searchParams.get('tab') as 'general' | 'seo' | 'og' | 'schema' | 'integrations' | 'content-types') || 'general';
|
||||
const [activeTab, setActiveTab] = useState<'general' | 'seo' | 'og' | 'schema' | 'integrations' | 'content-types'>(initialTab);
|
||||
const initialTab = (searchParams.get('tab') as 'general' | 'integrations' | 'content-types') || 'general';
|
||||
const [activeTab, setActiveTab] = useState<'general' | 'integrations' | 'content-types'>(initialTab);
|
||||
const [contentTypes, setContentTypes] = useState<any>(null);
|
||||
const [contentTypesLoading, setContentTypesLoading] = useState(false);
|
||||
|
||||
// Sectors selection state
|
||||
const [industries, setIndustries] = useState<Industry[]>([]);
|
||||
const [selectedIndustry, setSelectedIndustry] = useState<string>('');
|
||||
const [selectedSectors, setSelectedSectors] = useState<string[]>([]);
|
||||
const [isSelectingSectors, setIsSelectingSectors] = useState(false);
|
||||
const [userPreferences, setUserPreferences] = useState<{
|
||||
selectedIndustry?: string;
|
||||
selectedSectors?: string[];
|
||||
} | null>(null);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
slug: '',
|
||||
@@ -78,13 +95,14 @@ export default function SiteSettings() {
|
||||
// Load new site data
|
||||
loadSite();
|
||||
loadIntegrations();
|
||||
loadIndustries();
|
||||
}
|
||||
}, [siteId]);
|
||||
|
||||
useEffect(() => {
|
||||
// Update tab if URL parameter changes
|
||||
const tab = searchParams.get('tab');
|
||||
if (tab && ['general', 'seo', 'og', 'schema', 'integrations', 'content-types'].includes(tab)) {
|
||||
if (tab && ['general', 'integrations', 'content-types'].includes(tab)) {
|
||||
setActiveTab(tab as typeof activeTab);
|
||||
}
|
||||
}, [searchParams]);
|
||||
@@ -98,8 +116,16 @@ export default function SiteSettings() {
|
||||
// Load sites for selector
|
||||
useEffect(() => {
|
||||
loadSites();
|
||||
loadUserPreferences();
|
||||
}, []);
|
||||
|
||||
// Load site sectors when site and industries are loaded
|
||||
useEffect(() => {
|
||||
if (site && industries.length > 0) {
|
||||
loadSiteSectors();
|
||||
}
|
||||
}, [site, industries]);
|
||||
|
||||
const loadSites = async () => {
|
||||
try {
|
||||
setSitesLoading(true);
|
||||
@@ -171,6 +197,110 @@ export default function SiteSettings() {
|
||||
}
|
||||
};
|
||||
|
||||
const loadIndustries = async () => {
|
||||
try {
|
||||
const response = await fetchIndustries();
|
||||
let allIndustries = response.industries || [];
|
||||
|
||||
// Filter to show only user's pre-selected industries/sectors from account preferences
|
||||
try {
|
||||
const { fetchAccountSetting } = await import('../../services/api');
|
||||
const setting = await fetchAccountSetting('user_preferences');
|
||||
const preferences = setting.config as { selectedIndustry?: string; selectedSectors?: string[] } | undefined;
|
||||
|
||||
if (preferences?.selectedIndustry) {
|
||||
// Filter industries to only show the user's pre-selected industry
|
||||
allIndustries = allIndustries.filter(i => i.slug === preferences.selectedIndustry);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Silently handle errors - show all industries
|
||||
}
|
||||
|
||||
setIndustries(allIndustries);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load industries:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadUserPreferences = async () => {
|
||||
try {
|
||||
const { fetchAccountSetting } = await import('../../services/api');
|
||||
const setting = await fetchAccountSetting('user_preferences');
|
||||
const preferences = setting.config as { selectedIndustry?: string; selectedSectors?: string[] } | undefined;
|
||||
if (preferences) {
|
||||
setUserPreferences(preferences);
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Silently handle errors
|
||||
}
|
||||
};
|
||||
|
||||
const loadSiteSectors = async () => {
|
||||
if (!siteId) return;
|
||||
try {
|
||||
const { fetchSiteSectors } = await import('../../services/api');
|
||||
const sectors = await fetchSiteSectors(Number(siteId));
|
||||
const sectorSlugs = sectors.map((s: any) => s.slug);
|
||||
setSelectedSectors(sectorSlugs);
|
||||
|
||||
if (site?.industry_slug) {
|
||||
setSelectedIndustry(site.industry_slug);
|
||||
} else {
|
||||
for (const industry of industries) {
|
||||
const matchingSectors = industry.sectors.filter(s => sectorSlugs.includes(s.slug));
|
||||
if (matchingSectors.length > 0) {
|
||||
setSelectedIndustry(industry.slug);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Failed to load site sectors:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const getIndustrySectors = () => {
|
||||
if (!selectedIndustry) return [];
|
||||
const industry = industries.find(i => i.slug === selectedIndustry);
|
||||
let sectors = industry?.sectors || [];
|
||||
|
||||
// Filter to show only user's pre-selected sectors from account preferences
|
||||
if (userPreferences?.selectedSectors && userPreferences.selectedSectors.length > 0) {
|
||||
sectors = sectors.filter(s => userPreferences.selectedSectors!.includes(s.slug));
|
||||
}
|
||||
|
||||
return sectors;
|
||||
};
|
||||
|
||||
const handleSelectSectors = async () => {
|
||||
if (!siteId || !selectedIndustry || selectedSectors.length === 0) {
|
||||
toast.error('Please select an industry and at least one sector');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedSectors.length > 5) {
|
||||
toast.error('Maximum 5 sectors allowed per site');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSelectingSectors(true);
|
||||
const { selectSectorsForSite } = await import('../../services/api');
|
||||
await selectSectorsForSite(
|
||||
Number(siteId),
|
||||
selectedIndustry,
|
||||
selectedSectors
|
||||
);
|
||||
toast.success('Sectors configured successfully');
|
||||
await loadSite();
|
||||
await loadSiteSectors();
|
||||
} catch (error: any) {
|
||||
toast.error(`Failed to configure sectors: ${error.message}`);
|
||||
} finally {
|
||||
setIsSelectingSectors(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleIntegrationUpdate = async (integration: SiteIntegration) => {
|
||||
setWordPressIntegration(integration);
|
||||
await loadIntegrations();
|
||||
@@ -447,51 +577,6 @@ export default function SiteSettings() {
|
||||
<GridIcon className="w-4 h-4 inline mr-2" />
|
||||
General
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveTab('seo');
|
||||
navigate(`/sites/${siteId}/settings?tab=seo`, { replace: true });
|
||||
}}
|
||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'seo'
|
||||
? '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'
|
||||
}`}
|
||||
>
|
||||
<DocsIcon className="w-4 h-4 inline mr-2" />
|
||||
SEO Meta Tags
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveTab('og');
|
||||
navigate(`/sites/${siteId}/settings?tab=og`, { replace: true });
|
||||
}}
|
||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'og'
|
||||
? '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'
|
||||
}`}
|
||||
>
|
||||
<PaperPlaneIcon className="w-4 h-4 inline mr-2" />
|
||||
Open Graph
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setActiveTab('schema');
|
||||
navigate(`/sites/${siteId}/settings?tab=schema`, { replace: true });
|
||||
}}
|
||||
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'schema'
|
||||
? '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'
|
||||
}`}
|
||||
>
|
||||
<BoltIcon className="w-4 h-4 inline mr-2" />
|
||||
Schema.org
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -632,66 +717,373 @@ export default function SiteSettings() {
|
||||
<div className="space-y-6">
|
||||
{/* General Tab */}
|
||||
{activeTab === 'general' && (
|
||||
<Card className="p-6">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Site Name</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
{/* 4-Card Layout for Basic Settings, SEO, Open Graph, and Schema */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
{/* Card 1: Basic Site Settings */}
|
||||
<Card className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
||||
<GridIcon className="w-5 h-5 text-brand-500" />
|
||||
Basic Settings
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Site Name</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="mt-1 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>Slug</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.slug}
|
||||
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
|
||||
className="mt-1 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>Slug</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.slug}
|
||||
onChange={(e) => setFormData({ ...formData, slug: e.target.value })}
|
||||
className="mt-1 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>Site URL</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.site_url}
|
||||
onChange={(e) => setFormData({ ...formData, site_url: e.target.value })}
|
||||
placeholder="https://example.com"
|
||||
className="mt-1 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>Site URL</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.site_url}
|
||||
onChange={(e) => setFormData({ ...formData, site_url: e.target.value })}
|
||||
placeholder="https://example.com"
|
||||
className="mt-1 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>Site Type</Label>
|
||||
<SelectDropdown
|
||||
options={SITE_TYPES}
|
||||
value={formData.site_type}
|
||||
onChange={(value) => setFormData({ ...formData, site_type: value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Site Type</Label>
|
||||
<SelectDropdown
|
||||
options={SITE_TYPES}
|
||||
value={formData.site_type}
|
||||
onChange={(value) => setFormData({ ...formData, site_type: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Hosting Type</Label>
|
||||
<SelectDropdown
|
||||
options={HOSTING_TYPES}
|
||||
value={formData.hosting_type}
|
||||
onChange={(value) => setFormData({ ...formData, hosting_type: value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Hosting Type</Label>
|
||||
<SelectDropdown
|
||||
options={HOSTING_TYPES}
|
||||
value={formData.hosting_type}
|
||||
onChange={(value) => setFormData({ ...formData, hosting_type: value })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Checkbox
|
||||
checked={formData.is_active}
|
||||
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
|
||||
label="Active"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Checkbox
|
||||
checked={formData.is_active}
|
||||
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
|
||||
label="Active"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Card 2: SEO Meta Tags */}
|
||||
<Card className="p-6">
|
||||
<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
|
||||
</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Meta Title</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.meta_title}
|
||||
onChange={(e) => setFormData({ ...formData, meta_title: e.target.value })}
|
||||
placeholder="SEO title (recommended: 50-60 characters)"
|
||||
maxLength={60}
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{formData.meta_title.length}/60 characters
|
||||
</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>
|
||||
|
||||
<div>
|
||||
<Label>Meta Keywords (comma-separated)</Label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.meta_keywords}
|
||||
onChange={(e) => setFormData({ ...formData, meta_keywords: e.target.value })}
|
||||
placeholder="keyword1, keyword2, keyword3"
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.og_title}
|
||||
onChange={(e) => setFormData({ ...formData, og_title: e.target.value })}
|
||||
placeholder="Open Graph title"
|
||||
className="mt-1 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>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>
|
||||
<input
|
||||
type="url"
|
||||
value={formData.og_image}
|
||||
onChange={(e) => setFormData({ ...formData, og_image: e.target.value })}
|
||||
placeholder="https://example.com/image.jpg"
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
<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>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.og_site_name}
|
||||
onChange={(e) => setFormData({ ...formData, og_site_name: e.target.value })}
|
||||
placeholder="Site name for social sharing"
|
||||
className="mt-1 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>
|
||||
</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>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.schema_name}
|
||||
onChange={(e) => setFormData({ ...formData, schema_name: e.target.value })}
|
||||
placeholder="Organization name"
|
||||
className="mt-1 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>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>
|
||||
<input
|
||||
type="url"
|
||||
value={formData.schema_url}
|
||||
onChange={(e) => setFormData({ ...formData, schema_url: e.target.value })}
|
||||
placeholder="https://example.com"
|
||||
className="mt-1 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>Schema Logo URL</Label>
|
||||
<input
|
||||
type="url"
|
||||
value={formData.schema_logo}
|
||||
onChange={(e) => setFormData({ ...formData, schema_logo: e.target.value })}
|
||||
placeholder="https://example.com/logo.png"
|
||||
className="mt-1 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>Same As URLs (comma-separated)</Label>
|
||||
<input
|
||||
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"
|
||||
className="mt-1 w-full px-3 py-2 border border-gray-300 dark:border-gray-700 rounded-md dark:bg-gray-800 dark:text-white"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Social media profiles and other related URLs
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 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
|
||||
value={selectedIndustry}
|
||||
onChange={(e) => {
|
||||
setSelectedIndustry(e.target.value);
|
||||
setSelectedSectors([]);
|
||||
}}
|
||||
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
|
||||
>
|
||||
<option value="">Select an industry...</option>
|
||||
{industries.map((industry) => (
|
||||
<option key={industry.slug} value={industry.slug}>
|
||||
{industry.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{selectedIndustry && (
|
||||
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{industries.find(i => i.slug === selectedIndustry)?.description}
|
||||
</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-64 overflow-y-auto border border-gray-200 rounded-lg p-4 dark:border-gray-700">
|
||||
{getIndustrySectors().map((sector) => (
|
||||
<label
|
||||
key={sector.slug}
|
||||
className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedSectors.includes(sector.slug)}
|
||||
onChange={(e) => {
|
||||
if (e.target.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));
|
||||
}
|
||||
}}
|
||||
className="mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
|
||||
/>
|
||||
<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>
|
||||
</label>
|
||||
))}
|
||||
</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>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* SEO Meta Tags Tab */}
|
||||
@@ -905,7 +1297,7 @@ export default function SiteSettings() {
|
||||
)}
|
||||
|
||||
{/* Save Button */}
|
||||
{activeTab !== 'integrations' && activeTab !== 'content-types' && (
|
||||
{activeTab === 'general' && (
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSave} variant="primary" disabled={saving}>
|
||||
{saving ? 'Saving...' : 'Save Settings'}
|
||||
|
||||
Reference in New Issue
Block a user