final section 10 -- and lgoabl styles adn compoeents plan
This commit is contained in:
@@ -1,284 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Typography,
|
||||
Box,
|
||||
LinearProgress,
|
||||
Alert,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemIcon,
|
||||
CircularProgress,
|
||||
Chip
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Publish as PublishIcon,
|
||||
CheckCircle as SuccessIcon,
|
||||
Error as ErrorIcon,
|
||||
Schedule as PendingIcon
|
||||
} from '@mui/icons-material';
|
||||
import { api } from '../../services/api';
|
||||
|
||||
interface BulkWordPressPublishProps {
|
||||
selectedContentIds: string[];
|
||||
contentItems: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
}>;
|
||||
onPublishComplete: () => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
interface BulkPublishResult {
|
||||
total: number;
|
||||
queued: number;
|
||||
skipped: number;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export const BulkWordPressPublish: React.FC<BulkWordPressPublishProps> = ({
|
||||
selectedContentIds,
|
||||
contentItems,
|
||||
onPublishComplete,
|
||||
onClose
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [publishing, setPublishing] = useState(false);
|
||||
const [result, setResult] = useState<BulkPublishResult | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const selectedItems = contentItems.filter(item =>
|
||||
selectedContentIds.includes(item.id)
|
||||
);
|
||||
|
||||
const handleBulkPublish = async () => {
|
||||
setPublishing(true);
|
||||
setError(null);
|
||||
setResult(null);
|
||||
|
||||
try {
|
||||
const response = await api.post('/api/v1/content/bulk-publish-to-wordpress/', {
|
||||
content_ids: selectedContentIds.map(id => parseInt(id))
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
setResult({
|
||||
total: selectedContentIds.length,
|
||||
queued: response.data.data.content_count,
|
||||
skipped: 0,
|
||||
errors: []
|
||||
});
|
||||
|
||||
// Start polling for individual status updates
|
||||
startStatusPolling();
|
||||
} else {
|
||||
setError(response.data.message || 'Failed to start bulk publishing');
|
||||
}
|
||||
} catch (error: any) {
|
||||
setError(error.response?.data?.message || 'Error starting bulk publish');
|
||||
} finally {
|
||||
setPublishing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const startStatusPolling = () => {
|
||||
// Poll for 2 minutes to check status
|
||||
const pollInterval = setInterval(async () => {
|
||||
try {
|
||||
// Check status of all items (this could be optimized with a dedicated endpoint)
|
||||
const statusPromises = selectedContentIds.map(id =>
|
||||
api.get(`/api/v1/content/${id}/wordpress-status/`)
|
||||
);
|
||||
|
||||
const responses = await Promise.allSettled(statusPromises);
|
||||
|
||||
let completedCount = 0;
|
||||
let successCount = 0;
|
||||
let failedCount = 0;
|
||||
|
||||
responses.forEach((response) => {
|
||||
if (response.status === 'fulfilled' && response.value.data.success) {
|
||||
const status = response.value.data.data.wordpress_sync_status;
|
||||
if (status === 'success' || status === 'failed') {
|
||||
completedCount++;
|
||||
if (status === 'success') successCount++;
|
||||
if (status === 'failed') failedCount++;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If all items are complete, stop polling
|
||||
if (completedCount === selectedContentIds.length) {
|
||||
clearInterval(pollInterval);
|
||||
setResult(prev => prev ? {
|
||||
...prev,
|
||||
queued: successCount,
|
||||
errors: Array(failedCount).fill('Publishing failed')
|
||||
} : null);
|
||||
onPublishComplete();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error polling status:', error);
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
// Stop polling after 2 minutes
|
||||
setTimeout(() => {
|
||||
clearInterval(pollInterval);
|
||||
}, 120000);
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
setOpen(true);
|
||||
setResult(null);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const getResultSummary = () => {
|
||||
if (!result) return null;
|
||||
|
||||
const { total, queued, skipped, errors } = result;
|
||||
const failed = errors.length;
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Bulk Publish Results
|
||||
</Typography>
|
||||
|
||||
<Box display="flex" gap={1} flexWrap="wrap" mb={2}>
|
||||
<Chip
|
||||
icon={<SuccessIcon />}
|
||||
label={`${queued} Queued`}
|
||||
color="success"
|
||||
size="small"
|
||||
/>
|
||||
{skipped > 0 && (
|
||||
<Chip
|
||||
icon={<PendingIcon />}
|
||||
label={`${skipped} Skipped`}
|
||||
color="warning"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
{failed > 0 && (
|
||||
<Chip
|
||||
icon={<ErrorIcon />}
|
||||
label={`${failed} Failed`}
|
||||
color="error"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{failed > 0 && (
|
||||
<Alert severity="warning" sx={{ mt: 1 }}>
|
||||
Some items failed to publish. Check individual item status for details.
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<PublishIcon />}
|
||||
onClick={handleOpen}
|
||||
disabled={selectedContentIds.length === 0}
|
||||
>
|
||||
Bulk Publish to Site ({selectedContentIds.length})
|
||||
</Button>
|
||||
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
>
|
||||
<DialogTitle>
|
||||
Bulk Publish to Site
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
{!publishing && !result && (
|
||||
<>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
You are about to publish {selectedContentIds.length} content items to your site:
|
||||
</Typography>
|
||||
|
||||
<List dense sx={{ maxHeight: 300, overflow: 'auto', mt: 2 }}>
|
||||
{selectedItems.map((item) => (
|
||||
<ListItem key={item.id}>
|
||||
<ListItemIcon>
|
||||
<PublishIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={item.title}
|
||||
secondary={`Status: ${item.status}`}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
|
||||
<Alert severity="info" sx={{ mt: 2 }}>
|
||||
This will create new posts on your WordPress site with all content,
|
||||
images, categories, and SEO metadata. Items already published will be skipped.
|
||||
</Alert>
|
||||
</>
|
||||
)}
|
||||
|
||||
{publishing && (
|
||||
<Box sx={{ py: 3 }}>
|
||||
<Box display="flex" alignItems="center" gap={2} mb={2}>
|
||||
<CircularProgress size={24} />
|
||||
<Typography>Queuing content for WordPress publishing...</Typography>
|
||||
</Box>
|
||||
<LinearProgress />
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{result && getResultSummary()}
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mt: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
<Button onClick={handleClose}>
|
||||
{result ? 'Close' : 'Cancel'}
|
||||
</Button>
|
||||
|
||||
{!publishing && !result && (
|
||||
<Button
|
||||
onClick={handleBulkPublish}
|
||||
color="primary"
|
||||
variant="contained"
|
||||
disabled={selectedContentIds.length === 0}
|
||||
>
|
||||
Publish All to WordPress
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default BulkWordPressPublish;
|
||||
@@ -1,24 +1,20 @@
|
||||
/**
|
||||
* BulkWordPressPublish Component
|
||||
*
|
||||
* Handles bulk publishing multiple content items to connected WordPress sites.
|
||||
*
|
||||
* 🔒 STYLE LOCKED - Uses only standard ui/ components. See DESIGN_SYSTEM.md
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '../ui/button/Button';
|
||||
import { Modal } from '../ui/modal';
|
||||
import Alert from '../ui/alert/Alert';
|
||||
import { Spinner } from '../ui/spinner/Spinner';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Typography,
|
||||
Alert,
|
||||
Box,
|
||||
CircularProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Publish as PublishIcon,
|
||||
CheckCircle as SuccessIcon,
|
||||
Error as ErrorIcon
|
||||
} from '@mui/icons-material';
|
||||
CheckCircleIcon,
|
||||
ErrorIcon,
|
||||
PaperPlaneIcon,
|
||||
} from '../../icons';
|
||||
import { api } from '../../services/api';
|
||||
|
||||
interface BulkWordPressPublishProps {
|
||||
@@ -121,140 +117,140 @@ export const BulkWordPressPublish: React.FC<BulkWordPressPublishProps> = ({
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<PublishIcon />}
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
startIcon={<PaperPlaneIcon className="h-4 w-4" />}
|
||||
onClick={() => setOpen(true)}
|
||||
size="small"
|
||||
size="sm"
|
||||
>
|
||||
Publish Ready ({readyToPublish.length})
|
||||
</Button>
|
||||
|
||||
<Dialog
|
||||
open={open}
|
||||
<Modal
|
||||
isOpen={open}
|
||||
onClose={handleClose}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
disableEscapeKeyDown={publishing}
|
||||
className="max-w-2xl p-6"
|
||||
>
|
||||
<DialogTitle>
|
||||
Bulk Publish to Site
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent>
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Bulk Publish to Site
|
||||
</h2>
|
||||
|
||||
{!publishing && results.length === 0 && (
|
||||
<>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
Ready to publish <strong>{readyToPublish.length}</strong> content items to your site:
|
||||
</Typography>
|
||||
</p>
|
||||
|
||||
<Alert severity="info" sx={{ mt: 2, mb: 2 }}>
|
||||
Only content with generated images and not yet published will be included.
|
||||
</Alert>
|
||||
<Alert
|
||||
variant="info"
|
||||
title="Info"
|
||||
message="Only content with generated images and not yet published will be included."
|
||||
/>
|
||||
|
||||
<Box sx={{ maxHeight: 300, overflow: 'auto', border: 1, borderColor: 'divider', borderRadius: 1 }}>
|
||||
<List dense>
|
||||
{readyToPublish.map((item, index) => (
|
||||
<div key={item.id}>
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary={item.title}
|
||||
secondary={`ID: ${item.id}`}
|
||||
/>
|
||||
</ListItem>
|
||||
{index < readyToPublish.length - 1 && <Divider />}
|
||||
<div className="max-h-72 overflow-auto rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
{readyToPublish.map((item, index) => (
|
||||
<div key={item.id}>
|
||||
<div className="px-4 py-3">
|
||||
<p className="font-medium text-gray-900 dark:text-white">{item.title}</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">ID: {item.id}</p>
|
||||
</div>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
{index < readyToPublish.length - 1 && (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{publishing && (
|
||||
<Box display="flex" alignItems="center" gap={2} py={4}>
|
||||
<CircularProgress />
|
||||
<Typography>
|
||||
<div className="flex items-center gap-3 py-8">
|
||||
<Spinner size="md" />
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
Publishing {readyToPublish.length} items to WordPress...
|
||||
</Typography>
|
||||
</Box>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!publishing && results.length > 0 && (
|
||||
<>
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<div className="space-y-2">
|
||||
{successCount > 0 && (
|
||||
<Alert severity="success" sx={{ mb: 1 }}>
|
||||
✓ Successfully published {successCount} items
|
||||
</Alert>
|
||||
<Alert
|
||||
variant="success"
|
||||
title="Published"
|
||||
message={`Successfully published ${successCount} items`}
|
||||
/>
|
||||
)}
|
||||
|
||||
{failedCount > 0 && (
|
||||
<Alert severity="error">
|
||||
✗ Failed to publish {failedCount} items
|
||||
</Alert>
|
||||
<Alert
|
||||
variant="error"
|
||||
title="Failed"
|
||||
message={`Failed to publish ${failedCount} items`}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Results:
|
||||
</Typography>
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white">Results:</h3>
|
||||
|
||||
<Box sx={{ maxHeight: 400, overflow: 'auto', border: 1, borderColor: 'divider', borderRadius: 1 }}>
|
||||
<List dense>
|
||||
{results.map((result, index) => (
|
||||
<div key={result.id}>
|
||||
<ListItem>
|
||||
<Box display="flex" alignItems="center" width="100%">
|
||||
{result.status === 'success' ? (
|
||||
<SuccessIcon color="success" sx={{ mr: 1 }} />
|
||||
) : (
|
||||
<ErrorIcon color="error" sx={{ mr: 1 }} />
|
||||
)}
|
||||
<ListItemText
|
||||
primary={result.title}
|
||||
secondary={result.message || (result.status === 'success' ? 'Published successfully' : 'Publishing failed')}
|
||||
/>
|
||||
</Box>
|
||||
</ListItem>
|
||||
{index < results.length - 1 && <Divider />}
|
||||
<div className="max-h-96 overflow-auto rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
{results.map((result, index) => (
|
||||
<div key={result.id}>
|
||||
<div className="flex items-center gap-3 px-4 py-3">
|
||||
{result.status === 'success' ? (
|
||||
<CheckCircleIcon className="h-5 w-5 text-success-500" />
|
||||
) : (
|
||||
<ErrorIcon className="h-5 w-5 text-error-500" />
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<p className="font-medium text-gray-900 dark:text-white">{result.title}</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
{result.message || (result.status === 'success' ? 'Published successfully' : 'Publishing failed')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions>
|
||||
{!publishing && results.length === 0 && (
|
||||
<>
|
||||
<Button onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleBulkPublish}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<PublishIcon />}
|
||||
>
|
||||
Publish All ({readyToPublish.length})
|
||||
</Button>
|
||||
{index < results.length - 1 && (
|
||||
<div className="border-t border-gray-200 dark:border-gray-700" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{publishing && (
|
||||
<Button disabled>
|
||||
Publishing...
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!publishing && results.length > 0 && (
|
||||
<Button onClick={handleClose} variant="contained">
|
||||
Close
|
||||
</Button>
|
||||
)}
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<div className="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
{!publishing && results.length === 0 && (
|
||||
<>
|
||||
<Button variant="outline" tone="neutral" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
onClick={handleBulkPublish}
|
||||
startIcon={<PaperPlaneIcon className="h-4 w-4" />}
|
||||
>
|
||||
Publish All ({readyToPublish.length})
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{publishing && (
|
||||
<Button variant="outline" tone="neutral" disabled>
|
||||
Publishing...
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!publishing && results.length > 0 && (
|
||||
<Button variant="primary" tone="brand" onClick={handleClose}>
|
||||
Close
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,20 +1,21 @@
|
||||
import React, { useState } from 'react';
|
||||
/**
|
||||
* ContentActionsMenu Component
|
||||
*
|
||||
* Dropdown menu for content actions (edit, generate images, export, delete, publish).
|
||||
*
|
||||
* 🔒 STYLE LOCKED - Uses only standard ui/ components. See DESIGN_SYSTEM.md
|
||||
*/
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Button } from '../ui/button/Button';
|
||||
import { Dropdown } from '../ui/dropdown/Dropdown';
|
||||
import {
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Divider
|
||||
} from '@mui/material';
|
||||
import {
|
||||
MoreVert as MoreVertIcon,
|
||||
Publish as PublishIcon,
|
||||
Edit as EditIcon,
|
||||
Image as ImageIcon,
|
||||
GetApp as ExportIcon,
|
||||
Delete as DeleteIcon
|
||||
} from '@mui/icons-material';
|
||||
MoreDotIcon,
|
||||
PaperPlaneIcon,
|
||||
PencilIcon,
|
||||
FileIcon,
|
||||
DownloadIcon,
|
||||
TrashBinIcon,
|
||||
} from '../../icons';
|
||||
import { WordPressPublish } from './WordPressPublish';
|
||||
|
||||
interface ContentActionsMenuProps {
|
||||
@@ -40,16 +41,12 @@ export const ContentActionsMenu: React.FC<ContentActionsMenuProps> = ({
|
||||
onDelete,
|
||||
onWordPressStatusChange
|
||||
}) => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [showWordPressDialog, setShowWordPressDialog] = useState(false);
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const handlePublishClick = () => {
|
||||
@@ -69,85 +66,85 @@ export const ContentActionsMenu: React.FC<ContentActionsMenuProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
<button
|
||||
ref={buttonRef}
|
||||
aria-label="more actions"
|
||||
id="content-actions-button"
|
||||
aria-controls={open ? 'content-actions-menu' : undefined}
|
||||
aria-expanded={open ? 'true' : undefined}
|
||||
aria-haspopup="true"
|
||||
onClick={handleClick}
|
||||
size="small"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="dropdown-toggle flex h-8 w-8 items-center justify-center rounded-lg text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-white"
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
<MoreDotIcon className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<Menu
|
||||
id="content-actions-menu"
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
<Dropdown
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
MenuListProps={{
|
||||
'aria-labelledby': 'content-actions-button',
|
||||
}}
|
||||
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
||||
anchorRef={buttonRef as React.RefObject<HTMLElement>}
|
||||
placement="bottom-right"
|
||||
className="w-48"
|
||||
>
|
||||
{/* WordPress Publishing - Only show if images are ready */}
|
||||
{canPublishToWordPress && (
|
||||
<>
|
||||
<MenuItem onClick={handlePublishClick}>
|
||||
<ListItemIcon>
|
||||
<PublishIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Publish to Site</ListItemText>
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Edit Action */}
|
||||
{onEdit && (
|
||||
<MenuItem onClick={() => handleMenuAction(onEdit)}>
|
||||
<ListItemIcon>
|
||||
<EditIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Edit</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{/* Generate Image Action */}
|
||||
{onGenerateImage && (
|
||||
<MenuItem onClick={() => handleMenuAction(onGenerateImage)}>
|
||||
<ListItemIcon>
|
||||
<ImageIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Generate Image Prompts</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{/* Export Action */}
|
||||
{onExport && (
|
||||
<MenuItem onClick={() => handleMenuAction(onExport)}>
|
||||
<ListItemIcon>
|
||||
<ExportIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Export</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{/* Delete Action */}
|
||||
{onDelete && (
|
||||
<>
|
||||
<Divider />
|
||||
<MenuItem onClick={() => handleMenuAction(onDelete)} sx={{ color: 'error.main' }}>
|
||||
<ListItemIcon>
|
||||
<DeleteIcon fontSize="small" color="error" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>Delete</ListItemText>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</Menu>
|
||||
<div className="py-1">
|
||||
{/* WordPress Publishing - Only show if images are ready */}
|
||||
{canPublishToWordPress && (
|
||||
<>
|
||||
<button
|
||||
onClick={handlePublishClick}
|
||||
className="flex w-full items-center gap-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
<PaperPlaneIcon className="h-4 w-4" />
|
||||
<span>Publish to Site</span>
|
||||
</button>
|
||||
<div className="my-1 border-t border-gray-200 dark:border-gray-700" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Edit Action */}
|
||||
{onEdit && (
|
||||
<button
|
||||
onClick={() => handleMenuAction(onEdit)}
|
||||
className="flex w-full items-center gap-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
<PencilIcon className="h-4 w-4" />
|
||||
<span>Edit</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Generate Image Action */}
|
||||
{onGenerateImage && (
|
||||
<button
|
||||
onClick={() => handleMenuAction(onGenerateImage)}
|
||||
className="flex w-full items-center gap-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
<FileIcon className="h-4 w-4" />
|
||||
<span>Generate Image Prompts</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Export Action */}
|
||||
{onExport && (
|
||||
<button
|
||||
onClick={() => handleMenuAction(onExport)}
|
||||
className="flex w-full items-center gap-3 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-800"
|
||||
>
|
||||
<DownloadIcon className="h-4 w-4" />
|
||||
<span>Export</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Delete Action */}
|
||||
{onDelete && (
|
||||
<>
|
||||
<div className="my-1 border-t border-gray-200 dark:border-gray-700" />
|
||||
<button
|
||||
onClick={() => handleMenuAction(onDelete)}
|
||||
className="flex w-full items-center gap-3 px-4 py-2 text-sm text-error-600 hover:bg-error-50 dark:text-error-400 dark:hover:bg-error-500/10"
|
||||
>
|
||||
<TrashBinIcon className="h-4 w-4" />
|
||||
<span>Delete</span>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Dropdown>
|
||||
|
||||
{/* WordPress Publish Dialog */}
|
||||
{showWordPressDialog && (
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
/**
|
||||
* WordPressPublish Component
|
||||
*
|
||||
* Handles publishing content to connected WordPress sites.
|
||||
*
|
||||
* 🔒 STYLE LOCKED - Uses only standard ui/ components. See DESIGN_SYSTEM.md
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Alert,
|
||||
Chip,
|
||||
IconButton,
|
||||
Tooltip,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
CircularProgress,
|
||||
Box,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import {
|
||||
Publish as PublishIcon,
|
||||
Refresh as RefreshIcon,
|
||||
CheckCircle as SuccessIcon,
|
||||
Error as ErrorIcon,
|
||||
Schedule as PendingIcon,
|
||||
Sync as SyncingIcon
|
||||
} from '@mui/icons-material';
|
||||
import { Button } from '../ui/button/Button';
|
||||
import { Modal } from '../ui/modal';
|
||||
import Alert from '../ui/alert/Alert';
|
||||
import { Badge } from '../ui/badge/Badge';
|
||||
import { Tooltip } from '../ui/tooltip/Tooltip';
|
||||
import { Spinner } from '../ui/spinner/Spinner';
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
ErrorIcon,
|
||||
TimeIcon,
|
||||
PaperPlaneIcon,
|
||||
} from '../../icons';
|
||||
import { api } from '../../services/api';
|
||||
|
||||
export interface WordPressPublishProps {
|
||||
@@ -149,8 +146,9 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
|
||||
const getStatusInfo = () => {
|
||||
if (!wpStatus) {
|
||||
return {
|
||||
color: 'default' as const,
|
||||
icon: <PublishIcon />,
|
||||
tone: 'neutral' as const,
|
||||
badgeTone: 'neutral' as const,
|
||||
icon: <PaperPlaneIcon className="h-4 w-4" />,
|
||||
label: 'Not Published',
|
||||
action: 'publish'
|
||||
};
|
||||
@@ -159,36 +157,41 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
|
||||
switch (wpStatus.wordpress_sync_status) {
|
||||
case 'pending':
|
||||
return {
|
||||
color: 'warning' as const,
|
||||
icon: <PendingIcon />,
|
||||
tone: 'warning' as const,
|
||||
badgeTone: 'warning' as const,
|
||||
icon: <TimeIcon className="h-4 w-4" />,
|
||||
label: 'Queued',
|
||||
action: 'wait'
|
||||
};
|
||||
case 'syncing':
|
||||
return {
|
||||
color: 'info' as const,
|
||||
icon: <SyncingIcon className="animate-spin" />,
|
||||
tone: 'brand' as const,
|
||||
badgeTone: 'info' as const,
|
||||
icon: <Spinner size="sm" />,
|
||||
label: 'Publishing...',
|
||||
action: 'wait'
|
||||
};
|
||||
case 'success':
|
||||
return {
|
||||
color: 'success' as const,
|
||||
icon: <SuccessIcon />,
|
||||
tone: 'success' as const,
|
||||
badgeTone: 'success' as const,
|
||||
icon: <CheckCircleIcon className="h-4 w-4" />,
|
||||
label: 'Published',
|
||||
action: 'view'
|
||||
};
|
||||
case 'failed':
|
||||
return {
|
||||
color: 'error' as const,
|
||||
icon: <ErrorIcon />,
|
||||
tone: 'danger' as const,
|
||||
badgeTone: 'danger' as const,
|
||||
icon: <ErrorIcon className="h-4 w-4" />,
|
||||
label: 'Failed',
|
||||
action: 'retry'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
color: 'default' as const,
|
||||
icon: <PublishIcon />,
|
||||
tone: 'neutral' as const,
|
||||
badgeTone: 'neutral' as const,
|
||||
icon: <PaperPlaneIcon className="h-4 w-4" />,
|
||||
label: 'Not Published',
|
||||
action: 'publish'
|
||||
};
|
||||
@@ -202,181 +205,178 @@ export const WordPressPublish: React.FC<WordPressPublishProps> = ({
|
||||
|
||||
if (!shouldShowPublishButton) {
|
||||
return (
|
||||
<Tooltip title={`Images must be generated before publishing to site`}>
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<Tooltip text="Images must be generated before publishing to site">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outlined"
|
||||
variant="outline"
|
||||
tone="neutral"
|
||||
disabled
|
||||
size={size}
|
||||
startIcon={<PendingIcon />}
|
||||
size={size === 'small' ? 'sm' : size === 'large' ? 'lg' : 'md'}
|
||||
startIcon={<TimeIcon className="h-4 w-4" />}
|
||||
>
|
||||
Awaiting Images
|
||||
</Button>
|
||||
{size !== 'small' && (
|
||||
<Chip
|
||||
icon={<PendingIcon />}
|
||||
label="Images Pending"
|
||||
color="warning"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
/>
|
||||
<Badge
|
||||
tone="warning"
|
||||
variant="outline"
|
||||
startIcon={<TimeIcon className="h-3 w-3" />}
|
||||
>
|
||||
Images Pending
|
||||
</Badge>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
const renderButton = () => {
|
||||
if (size === 'small') {
|
||||
return (
|
||||
<Tooltip title={`Site: ${statusInfo.label}`}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
if (statusInfo.action === 'publish') {
|
||||
setPublishDialogOpen(true);
|
||||
} else if (statusInfo.action === 'retry') {
|
||||
handleRetry();
|
||||
} else if (statusInfo.action === 'view' && wpStatus?.wordpress_post_url) {
|
||||
window.open(wpStatus.wordpress_post_url, '_blank');
|
||||
}
|
||||
}}
|
||||
disabled={loading || statusInfo.action === 'wait'}
|
||||
color={statusInfo.color}
|
||||
>
|
||||
{loading ? <CircularProgress size={16} /> : statusInfo.icon}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
const handleButtonClick = () => {
|
||||
if (statusInfo.action === 'publish') {
|
||||
setPublishDialogOpen(true);
|
||||
} else if (statusInfo.action === 'retry') {
|
||||
handleRetry();
|
||||
} else if (statusInfo.action === 'view' && wpStatus?.wordpress_post_url) {
|
||||
window.open(wpStatus.wordpress_post_url, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
const renderButton = () => {
|
||||
const buttonSize = size === 'small' ? 'sm' : size === 'large' ? 'lg' : 'md';
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={statusInfo.action === 'publish' ? 'contained' : 'outlined'}
|
||||
color={statusInfo.color}
|
||||
startIcon={loading ? <CircularProgress size={20} /> : statusInfo.icon}
|
||||
onClick={() => {
|
||||
if (statusInfo.action === 'publish') {
|
||||
setPublishDialogOpen(true);
|
||||
} else if (statusInfo.action === 'retry') {
|
||||
handleRetry();
|
||||
} else if (statusInfo.action === 'view' && wpStatus?.wordpress_post_url) {
|
||||
window.open(wpStatus.wordpress_post_url, '_blank');
|
||||
}
|
||||
}}
|
||||
disabled={loading || statusInfo.action === 'wait'}
|
||||
size={size}
|
||||
>
|
||||
{statusInfo.action === 'publish' && 'Publish to Site'}
|
||||
{statusInfo.action === 'retry' && 'Retry'}
|
||||
{statusInfo.action === 'view' && 'View on Site'}
|
||||
{statusInfo.action === 'wait' && statusInfo.label}
|
||||
</Button>
|
||||
<Tooltip text={`Site: ${statusInfo.label}`}>
|
||||
<Button
|
||||
size={buttonSize}
|
||||
variant={statusInfo.action === 'publish' ? 'primary' : 'outline'}
|
||||
tone={statusInfo.tone}
|
||||
onClick={handleButtonClick}
|
||||
disabled={loading || statusInfo.action === 'wait'}
|
||||
startIcon={loading ? <Spinner size="sm" /> : statusInfo.icon}
|
||||
>
|
||||
{statusInfo.action === 'publish' && 'Publish to Site'}
|
||||
{statusInfo.action === 'retry' && 'Retry'}
|
||||
{statusInfo.action === 'view' && 'View on Site'}
|
||||
{statusInfo.action === 'wait' && statusInfo.label}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const renderStatusChip = () => {
|
||||
const renderStatusBadge = () => {
|
||||
if (size === 'small') return null;
|
||||
|
||||
return (
|
||||
<Chip
|
||||
icon={statusInfo.icon}
|
||||
label={statusInfo.label}
|
||||
color={statusInfo.color}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
if (wpStatus?.wordpress_post_url) {
|
||||
window.open(wpStatus.wordpress_post_url, '_blank');
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
marginLeft: 8,
|
||||
cursor: wpStatus?.wordpress_post_url ? 'pointer' : 'default'
|
||||
}}
|
||||
/>
|
||||
<Badge
|
||||
tone={statusInfo.badgeTone}
|
||||
variant="outline"
|
||||
startIcon={statusInfo.icon}
|
||||
className={wpStatus?.wordpress_post_url ? 'cursor-pointer' : ''}
|
||||
>
|
||||
<span
|
||||
onClick={() => {
|
||||
if (wpStatus?.wordpress_post_url) {
|
||||
window.open(wpStatus.wordpress_post_url, '_blank');
|
||||
}
|
||||
}}
|
||||
>
|
||||
{statusInfo.label}
|
||||
</span>
|
||||
</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<div className="flex items-center gap-2">
|
||||
{renderButton()}
|
||||
{renderStatusChip()}
|
||||
{renderStatusBadge()}
|
||||
|
||||
{error && (
|
||||
<Alert severity="error" sx={{ mt: 1 }}>
|
||||
{error}
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setError(null)}
|
||||
sx={{ ml: 1 }}
|
||||
>
|
||||
×
|
||||
</IconButton>
|
||||
</Alert>
|
||||
<div className="mt-2">
|
||||
<Alert
|
||||
variant="error"
|
||||
title="Publishing Error"
|
||||
message={error}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Publish Confirmation Dialog */}
|
||||
<Dialog
|
||||
open={publishDialogOpen}
|
||||
{/* Publish Confirmation Modal */}
|
||||
<Modal
|
||||
isOpen={publishDialogOpen}
|
||||
onClose={() => setPublishDialogOpen(false)}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
className="max-w-md p-6"
|
||||
>
|
||||
<DialogTitle>Publish to Site</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body1" gutterBottom>
|
||||
Are you sure you want to publish "<strong>{contentTitle}</strong>" to your site?
|
||||
</Typography>
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Publish to Site
|
||||
</h2>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mt: 2 }}>
|
||||
<p className="text-gray-700 dark:text-gray-300">
|
||||
Are you sure you want to publish "<strong>{contentTitle}</strong>" to your site?
|
||||
</p>
|
||||
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
This will create a new post on your connected site with all content,
|
||||
images, categories, and SEO metadata.
|
||||
</Typography>
|
||||
</p>
|
||||
|
||||
{imageGenerationStatus === 'complete' && (
|
||||
<Alert severity="success" sx={{ mt: 2 }}>
|
||||
✓ Images are generated and ready for publishing
|
||||
</Alert>
|
||||
<Alert
|
||||
variant="success"
|
||||
title="Ready to Publish"
|
||||
message="Images are generated and ready for publishing"
|
||||
/>
|
||||
)}
|
||||
|
||||
{imageGenerationStatus !== 'complete' && showOnlyIfImagesReady && (
|
||||
<Alert severity="warning" sx={{ mt: 2 }}>
|
||||
Images are still being generated. Please wait before publishing.
|
||||
</Alert>
|
||||
<Alert
|
||||
variant="warning"
|
||||
title="Images Not Ready"
|
||||
message="Images are still being generated. Please wait before publishing."
|
||||
/>
|
||||
)}
|
||||
|
||||
{wpStatus?.wordpress_sync_status === 'success' && (
|
||||
<Alert severity="info" sx={{ mt: 2 }}>
|
||||
This content is already published to your site.
|
||||
You can force republish to update the existing post.
|
||||
</Alert>
|
||||
<Alert
|
||||
variant="info"
|
||||
title="Already Published"
|
||||
message="This content is already published to your site. You can force republish to update the existing post."
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => setPublishDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
{wpStatus?.wordpress_sync_status === 'success' && (
|
||||
|
||||
<div className="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<Button
|
||||
onClick={() => handlePublishToWordPress(true)}
|
||||
color="warning"
|
||||
disabled={loading}
|
||||
variant="outline"
|
||||
tone="neutral"
|
||||
onClick={() => setPublishDialogOpen(false)}
|
||||
>
|
||||
Force Republish
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={() => handlePublishToWordPress(false)}
|
||||
color="primary"
|
||||
variant="contained"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Publishing...' : 'Publish'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</Box>
|
||||
|
||||
{wpStatus?.wordpress_sync_status === 'success' && (
|
||||
<Button
|
||||
variant="outline"
|
||||
tone="warning"
|
||||
onClick={() => handlePublishToWordPress(true)}
|
||||
disabled={loading}
|
||||
>
|
||||
Force Republish
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
variant="primary"
|
||||
tone="brand"
|
||||
onClick={() => handlePublishToWordPress(false)}
|
||||
disabled={loading}
|
||||
startIcon={loading ? <Spinner size="sm" /> : <PaperPlaneIcon className="h-4 w-4" />}
|
||||
>
|
||||
{loading ? 'Publishing...' : 'Publish'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ const Alert: React.FC<AlertProps> = ({
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M20.3499 12.0004C20.3499 16.612 16.6115 20.3504 11.9999 20.3504C7.38832 20.3504 3.6499 16.612 3.6499 12.0004C3.6499 7.38881 7.38833 3.65039 11.9999 3.65039C16.6115 3.65039 20.3499 7.38881 20.3499 12.0004ZM11.9999 22.1504C17.6056 22.1504 22.1499 17.6061 22.1499 12.0004C22.1499 6.3947 17.6056 1.85039 11.9999 1.85039C6.39421 1.85039 1.8499 6.3947 1.8499 12.0004C1.8499 17.6061 6.39421 22.1504 11.9999 22.1504ZM13.0008 16.4753C13.0008 15.923 12.5531 15.4753 12.0008 15.4753L11.9998 15.4753C11.4475 15.4753 10.9998 15.923 10.9998 16.4753C10.9998 17.0276 11.4475 17.4753 11.9998 17.4753L12.0008 17.4753C12.5531 17.4753 13.0008 17.0276 13.0008 16.4753ZM11.9998 6.62898C12.414 6.62898 12.7498 6.96476 12.7498 7.37898L12.7498 13.0555C12.7498 13.4697 12.414 13.8055 11.9998 13.8055C11.5856 13.8055 11.2498 13.4697 11.2498 13.0555L11.2498 7.37898C11.2498 6.96476 11.5856 6.62898 11.9998 6.62898Z"
|
||||
fill="#F04438"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user