245 lines
8.2 KiB
TypeScript
245 lines
8.2 KiB
TypeScript
/**
|
|
* Publishing Rules Component
|
|
* Phase 6: Site Integration & Multi-Destination Publishing
|
|
*/
|
|
import React, { useState } from 'react';
|
|
import { PlusIcon, TrashIcon, ArrowUpIcon, ArrowDownIcon } from '../../icons';
|
|
import Button from '../ui/button/Button';
|
|
import Checkbox from '../form/input/Checkbox';
|
|
import Label from '../form/Label';
|
|
import SelectDropdown from '../form/SelectDropdown';
|
|
|
|
export interface PublishingRule {
|
|
id: string;
|
|
content_type: string;
|
|
trigger: 'auto' | 'manual' | 'scheduled';
|
|
destinations: string[];
|
|
priority: number;
|
|
enabled: boolean;
|
|
schedule?: string; // Cron expression for scheduled
|
|
}
|
|
|
|
interface PublishingRulesProps {
|
|
rules: PublishingRule[];
|
|
onChange: (rules: PublishingRule[]) => void;
|
|
}
|
|
|
|
const CONTENT_TYPES = [
|
|
{ value: 'blog_post', label: 'Blog Post' },
|
|
{ value: 'page', label: 'Page' },
|
|
{ value: 'product', label: 'Product' },
|
|
{ value: 'all', label: 'All Content' },
|
|
];
|
|
|
|
const TRIGGERS = [
|
|
{ value: 'auto', label: 'Auto-Publish' },
|
|
{ value: 'manual', label: 'Manual' },
|
|
{ value: 'scheduled', label: 'Scheduled' },
|
|
];
|
|
|
|
const DESTINATIONS = [
|
|
{ value: 'sites', label: 'IGNY8 Sites' },
|
|
{ value: 'wordpress', label: 'WordPress' },
|
|
{ value: 'shopify', label: 'Shopify' },
|
|
];
|
|
|
|
export default function PublishingRules({ rules, onChange }: PublishingRulesProps) {
|
|
const [localRules, setLocalRules] = useState<PublishingRule[]>(rules);
|
|
|
|
const handleAddRule = () => {
|
|
const newRule: PublishingRule = {
|
|
id: `rule_${Date.now()}`,
|
|
content_type: 'all',
|
|
trigger: 'manual',
|
|
destinations: ['sites'],
|
|
priority: localRules.length + 1,
|
|
enabled: true,
|
|
};
|
|
const updated = [...localRules, newRule];
|
|
setLocalRules(updated);
|
|
onChange(updated);
|
|
};
|
|
|
|
const handleDeleteRule = (id: string) => {
|
|
const updated = localRules.filter((r) => r.id !== id);
|
|
setLocalRules(updated);
|
|
onChange(updated);
|
|
};
|
|
|
|
const handleUpdateRule = (id: string, field: keyof PublishingRule, value: any) => {
|
|
const updated = localRules.map((rule) =>
|
|
rule.id === id ? { ...rule, [field]: value } : rule
|
|
);
|
|
setLocalRules(updated);
|
|
onChange(updated);
|
|
};
|
|
|
|
const handleMoveRule = (id: string, direction: 'up' | 'down') => {
|
|
const index = localRules.findIndex((r) => r.id === id);
|
|
if (index === -1) return;
|
|
|
|
const newIndex = direction === 'up' ? index - 1 : index + 1;
|
|
if (newIndex < 0 || newIndex >= localRules.length) return;
|
|
|
|
const updated = [...localRules];
|
|
[updated[index], updated[newIndex]] = [updated[newIndex], updated[index]];
|
|
|
|
// Update priorities
|
|
updated.forEach((rule, i) => {
|
|
rule.priority = i + 1;
|
|
});
|
|
|
|
setLocalRules(updated);
|
|
onChange(updated);
|
|
};
|
|
|
|
const handleToggleDestinations = (ruleId: string, destination: string) => {
|
|
const rule = localRules.find((r) => r.id === ruleId);
|
|
if (!rule) return;
|
|
|
|
const destinations = rule.destinations.includes(destination)
|
|
? rule.destinations.filter((d) => d !== destination)
|
|
: [...rule.destinations, destination];
|
|
|
|
handleUpdateRule(ruleId, 'destinations', destinations);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">
|
|
Advanced Publishing Rules
|
|
</h3>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">
|
|
Set specific rules for different types of content
|
|
</p>
|
|
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
Example: Publish blog posts to WordPress but guides to your main site
|
|
</p>
|
|
</div>
|
|
<Button onClick={handleAddRule} variant="primary" size="sm" startIcon={<PlusIcon className="w-4 h-4" />}>
|
|
Add a Publishing Rule
|
|
</Button>
|
|
</div>
|
|
|
|
{localRules.length === 0 ? (
|
|
<div className="text-center py-8 border border-dashed border-gray-300 dark:border-gray-700 rounded-lg">
|
|
<p className="text-gray-600 dark:text-gray-400 mb-4">No publishing rules configured</p>
|
|
<Button onClick={handleAddRule} variant="outline">
|
|
Add Your First Rule
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{localRules.map((rule, index) => (
|
|
<div
|
|
key={rule.id}
|
|
className="border border-gray-200 dark:border-gray-700 rounded-lg p-4 space-y-3"
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex-1 grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<Label>Content Type</Label>
|
|
<SelectDropdown
|
|
options={CONTENT_TYPES}
|
|
value={rule.content_type}
|
|
onChange={(value) =>
|
|
handleUpdateRule(rule.id, 'content_type', value)
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Trigger</Label>
|
|
<SelectDropdown
|
|
options={TRIGGERS}
|
|
value={rule.trigger}
|
|
onChange={(value) =>
|
|
handleUpdateRule(rule.id, 'trigger', value)
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Priority</Label>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleMoveRule(rule.id, 'up')}
|
|
disabled={index === 0}
|
|
>
|
|
<ArrowUpIcon className="w-4 h-4" />
|
|
</Button>
|
|
<span className="text-sm font-medium">{rule.priority}</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleMoveRule(rule.id, 'down')}
|
|
disabled={index === localRules.length - 1}
|
|
>
|
|
<ArrowDownIcon className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 ml-4">
|
|
<Checkbox
|
|
checked={rule.enabled}
|
|
onChange={(e) =>
|
|
handleUpdateRule(rule.id, 'enabled', e.target.checked)
|
|
}
|
|
label="Enabled"
|
|
/>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleDeleteRule(rule.id)}
|
|
>
|
|
<TrashIcon className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<Label>Destinations</Label>
|
|
<div className="flex flex-wrap gap-3 mt-2">
|
|
{DESTINATIONS.map((dest) => (
|
|
<Checkbox
|
|
key={dest.value}
|
|
checked={rule.destinations.includes(dest.value)}
|
|
onChange={() => handleToggleDestinations(rule.id, dest.value)}
|
|
label={dest.label}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{rule.trigger === 'scheduled' && (
|
|
<div>
|
|
<Label>Schedule (Cron Expression)</Label>
|
|
<input
|
|
type="text"
|
|
value={rule.schedule || ''}
|
|
onChange={(e) =>
|
|
handleUpdateRule(rule.id, 'schedule', e.target.value)
|
|
}
|
|
placeholder="0 0 * * *"
|
|
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="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
Cron format: minute hour day month weekday
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|