final section 10 -- and lgoabl styles adn compoeents plan

This commit is contained in:
IGNY8 VPS (Salman)
2026-01-01 10:41:16 +00:00
parent 41e124d8e8
commit d389576634
18 changed files with 1918 additions and 1374 deletions

View File

@@ -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;

View File

@@ -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>
</>
);
};

View File

@@ -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 && (

View File

@@ -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>
);
};

View File

@@ -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>
),