488 lines
18 KiB
TypeScript
488 lines
18 KiB
TypeScript
/**
|
|
* Site Settings (Advanced)
|
|
* Phase 7: Advanced Site Management
|
|
* Features: SEO (meta tags, Open Graph, schema.org)
|
|
*/
|
|
import React, { useState, useEffect } from 'react';
|
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
import { SettingsIcon, SearchIcon, Share2Icon, CodeIcon } from 'lucide-react';
|
|
import PageMeta from '../../components/common/PageMeta';
|
|
import { Card } from '../../components/ui/card';
|
|
import Button from '../../components/ui/button/Button';
|
|
import Label from '../../components/form/Label';
|
|
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 } from '../../services/api';
|
|
|
|
export default function SiteSettings() {
|
|
const { siteId } = useParams<{ siteId: string }>();
|
|
const navigate = useNavigate();
|
|
const toast = useToast();
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [site, setSite] = useState<any>(null);
|
|
|
|
const [activeTab, setActiveTab] = useState<'general' | 'seo' | 'og' | 'schema'>('general');
|
|
const [formData, setFormData] = useState({
|
|
name: '',
|
|
slug: '',
|
|
site_type: 'marketing',
|
|
hosting_type: 'igny8_sites',
|
|
is_active: true,
|
|
// SEO fields
|
|
meta_title: '',
|
|
meta_description: '',
|
|
meta_keywords: '',
|
|
og_title: '',
|
|
og_description: '',
|
|
og_image: '',
|
|
og_type: 'website',
|
|
og_site_name: '',
|
|
schema_type: 'Organization',
|
|
schema_name: '',
|
|
schema_description: '',
|
|
schema_url: '',
|
|
schema_logo: '',
|
|
schema_same_as: '',
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (siteId) {
|
|
loadSite();
|
|
}
|
|
}, [siteId]);
|
|
|
|
const loadSite = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const data = await fetchAPI(`/v1/auth/sites/${siteId}/`);
|
|
if (data) {
|
|
setSite(data);
|
|
const seoData = data.seo_metadata || data.metadata || {};
|
|
setFormData({
|
|
name: data.name || '',
|
|
slug: data.slug || '',
|
|
site_type: data.site_type || 'marketing',
|
|
hosting_type: data.hosting_type || 'igny8_sites',
|
|
is_active: data.is_active !== false,
|
|
// SEO fields
|
|
meta_title: seoData.meta_title || data.name || '',
|
|
meta_description: seoData.meta_description || data.description || '',
|
|
meta_keywords: seoData.meta_keywords || '',
|
|
og_title: seoData.og_title || seoData.meta_title || data.name || '',
|
|
og_description: seoData.og_description || seoData.meta_description || data.description || '',
|
|
og_image: seoData.og_image || '',
|
|
og_type: seoData.og_type || 'website',
|
|
og_site_name: seoData.og_site_name || data.name || '',
|
|
schema_type: seoData.schema_type || 'Organization',
|
|
schema_name: seoData.schema_name || data.name || '',
|
|
schema_description: seoData.schema_description || data.description || '',
|
|
schema_url: seoData.schema_url || data.domain || '',
|
|
schema_logo: seoData.schema_logo || '',
|
|
schema_same_as: Array.isArray(seoData.schema_same_as) ? seoData.schema_same_as.join(', ') : seoData.schema_same_as || '',
|
|
});
|
|
}
|
|
} catch (error: any) {
|
|
toast.error(`Failed to load site: ${error.message}`);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSave = async () => {
|
|
try {
|
|
setSaving(true);
|
|
const { meta_title, meta_description, meta_keywords, og_title, og_description, og_image, og_type, og_site_name, schema_type, schema_name, schema_description, schema_url, schema_logo, schema_same_as, ...basicData } = formData;
|
|
|
|
const payload = {
|
|
...basicData,
|
|
seo_metadata: {
|
|
meta_title,
|
|
meta_description,
|
|
meta_keywords,
|
|
og_title,
|
|
og_description,
|
|
og_image,
|
|
og_type,
|
|
og_site_name,
|
|
schema_type,
|
|
schema_name,
|
|
schema_description,
|
|
schema_url,
|
|
schema_logo,
|
|
schema_same_as: schema_same_as ? schema_same_as.split(',').map((s) => s.trim()).filter(Boolean) : [],
|
|
},
|
|
};
|
|
|
|
await fetchAPI(`/v1/auth/sites/${siteId}/`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
toast.success('Site settings saved successfully');
|
|
loadSite();
|
|
} catch (error: any) {
|
|
toast.error(`Failed to save settings: ${error.message}`);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
const SITE_TYPES = [
|
|
{ value: 'marketing', label: 'Marketing Site' },
|
|
{ value: 'ecommerce', label: 'Ecommerce Site' },
|
|
{ value: 'blog', label: 'Blog' },
|
|
{ value: 'portfolio', label: 'Portfolio' },
|
|
{ value: 'corporate', label: 'Corporate' },
|
|
];
|
|
|
|
const HOSTING_TYPES = [
|
|
{ value: 'igny8_sites', label: 'IGNY8 Sites' },
|
|
{ value: 'wordpress', label: 'WordPress' },
|
|
{ value: 'shopify', label: 'Shopify' },
|
|
{ value: 'multi', label: 'Multi-Destination' },
|
|
];
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="p-6">
|
|
<PageMeta title="Site Settings" />
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="text-gray-500">Loading site settings...</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="p-6">
|
|
<PageMeta title="Site Settings - IGNY8" />
|
|
|
|
<div className="mb-6">
|
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
Site Settings
|
|
</h1>
|
|
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
|
Configure site type, hosting, and other settings
|
|
</p>
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<div className="mb-6 border-b border-gray-200 dark:border-gray-700">
|
|
<div className="flex gap-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveTab('general')}
|
|
className={`px-4 py-2 font-medium border-b-2 transition-colors ${
|
|
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'
|
|
}`}
|
|
>
|
|
<SettingsIcon className="w-4 h-4 inline mr-2" />
|
|
General
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveTab('seo')}
|
|
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'
|
|
}`}
|
|
>
|
|
<SearchIcon className="w-4 h-4 inline mr-2" />
|
|
SEO Meta Tags
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveTab('og')}
|
|
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'
|
|
}`}
|
|
>
|
|
<Share2Icon className="w-4 h-4 inline mr-2" />
|
|
Open Graph
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveTab('schema')}
|
|
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'
|
|
}`}
|
|
>
|
|
<CodeIcon className="w-4 h-4 inline mr-2" />
|
|
Schema.org
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<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>
|
|
|
|
<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 Type</Label>
|
|
<SelectDropdown
|
|
options={SITE_TYPES}
|
|
value={formData.site_type}
|
|
onChange={(e) => setFormData({ ...formData, site_type: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Hosting Type</Label>
|
|
<SelectDropdown
|
|
options={HOSTING_TYPES}
|
|
value={formData.hosting_type}
|
|
onChange={(e) => setFormData({ ...formData, hosting_type: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Checkbox
|
|
checked={formData.is_active}
|
|
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
|
|
label="Active"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
{/* SEO Meta Tags Tab */}
|
|
{activeTab === 'seo' && (
|
|
<Card className="p-6">
|
|
<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>
|
|
)}
|
|
|
|
{/* Open Graph Tab */}
|
|
{activeTab === 'og' && (
|
|
<Card className="p-6">
|
|
<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={4}
|
|
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={(e) => setFormData({ ...formData, og_type: e.target.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>
|
|
)}
|
|
|
|
{/* Schema.org Tab */}
|
|
{activeTab === 'schema' && (
|
|
<Card className="p-6">
|
|
<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={(e) => setFormData({ ...formData, schema_type: e.target.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 className="flex justify-end">
|
|
<Button onClick={handleSave} variant="primary" disabled={saving}>
|
|
{saving ? 'Saving...' : 'Save Settings'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|