Issue #518 - Subscription not updating after checkout: - Fix stale closure in SubscriptionContext checkout polling (use subscriptionRef) - Move checkout success polling from InitialRouteHandler into SubscriptionContext - Remove redundant polling code from InitialRouteHandler - Fix plan label: 'Free' instead of 'No Plan', proper capitalization - Add plan refresh button in UserBadge - Add 'View Costing Details' to UserBadge dropdown - Rename 'ALwrity Podcast Maker' to 'Podcast Creator' across UI - Clean subscription=success URL param after verification Blog Writer WYSIWYG Editor enhancements: - Per-section preview toggle (view/edit icons) - Enhanced hover-based toolbar - Circular SVG progress stats bar with detailed tooltip - Research tool chips in stats bar footer - Per-section TTS with useTextToSpeech hook (browser native) - Full blog preview modal with print/PDF support - PlayAllTTSButton: sequential playback with progress bar - OnThisPageNav: floating sidebar with scroll tracking - Section data attributes for scroll anchoring GSC Brainstorm Topics feature: - Backend: gsc_brainstorm_service.py (rule-based + LLM recommendations) - Backend: POST /gsc/brainstorm endpoint with 3-word minimum validation - Frontend: gscBrainstorm.ts API client - Frontend: useGSCBrainstormConnection hook (popup OAuth, no /onboarding redirect) - Frontend: useGSCBrainstorm hook (connect check + brainstorm call) - Frontend: GSCBrainstormModal (3-tab results: Opportunities, Gaps, AI Recs) - Frontend: BrainstormButton (visible at 3+ words, GSC connect overlay) - Wire BrainstormButton into ManualResearchForm and ResearchAction - Add blog_writer to gsc_auth router features for ALWRITY_ENABLED_FEATURES
637 lines
22 KiB
TypeScript
637 lines
22 KiB
TypeScript
/**
|
||
* SEO Metadata Modal Component
|
||
*
|
||
* Comprehensive SEO metadata generation and editing interface with:
|
||
* - Tabbed interface for different metadata types
|
||
* - Live preview of social media cards
|
||
* - Character counters and validation
|
||
* - Copy-to-clipboard functionality
|
||
* - Integration with backend metadata generation
|
||
*/
|
||
|
||
import React, { useState, useEffect, useCallback } from 'react';
|
||
import {
|
||
Dialog,
|
||
DialogTitle,
|
||
DialogContent,
|
||
DialogActions,
|
||
Button,
|
||
Box,
|
||
Typography,
|
||
Tabs,
|
||
Tab,
|
||
CircularProgress,
|
||
Alert,
|
||
IconButton,
|
||
Chip,
|
||
Tooltip
|
||
} from '@mui/material';
|
||
import {
|
||
Close as CloseIcon,
|
||
Check as CheckIcon,
|
||
Preview as PreviewIcon,
|
||
Search as SearchIcon,
|
||
Share as ShareIcon,
|
||
Code as CodeIcon,
|
||
Tag as TagIcon,
|
||
Refresh as RefreshIcon
|
||
} from '@mui/icons-material';
|
||
import { apiClient, triggerSubscriptionError } from '../../api/client';
|
||
|
||
// Import metadata display components
|
||
import { CoreMetadataTab } from './SEO/MetadataDisplay/CoreMetadataTab';
|
||
import { SocialMediaTab } from './SEO/MetadataDisplay/SocialMediaTab';
|
||
import { StructuredDataTab } from './SEO/MetadataDisplay/StructuredDataTab';
|
||
import { PreviewCard } from './SEO/MetadataDisplay/PreviewCard';
|
||
import { subscribeImage } from '../../utils/imageBus';
|
||
|
||
interface SEOMetadataModalProps {
|
||
isOpen: boolean;
|
||
onClose: () => void;
|
||
blogContent: string;
|
||
blogTitle: string;
|
||
researchData: any;
|
||
outline?: any[]; // Add outline structure
|
||
seoAnalysis?: any; // Add SEO analysis results
|
||
onMetadataGenerated: (metadata: any) => void;
|
||
}
|
||
|
||
interface SEOMetadataResult {
|
||
success: boolean;
|
||
seo_title?: string;
|
||
meta_description?: string;
|
||
url_slug?: string;
|
||
blog_tags?: string[];
|
||
blog_categories?: string[];
|
||
social_hashtags?: string[];
|
||
open_graph?: any;
|
||
twitter_card?: any;
|
||
json_ld_schema?: any;
|
||
canonical_url?: string;
|
||
reading_time?: number;
|
||
focus_keyword?: string;
|
||
generated_at?: string;
|
||
optimization_score?: number;
|
||
error?: string;
|
||
}
|
||
|
||
// Cache helper functions (similar to SEOAnalysisModal)
|
||
async function hashContent(text: string): Promise<string> {
|
||
try {
|
||
const enc = new TextEncoder().encode(text);
|
||
const digest = await crypto.subtle.digest('SHA-256', enc);
|
||
const bytes = Array.from(new Uint8Array(digest));
|
||
return bytes.map(b => b.toString(16).padStart(2, '0')).join('');
|
||
} catch {
|
||
// Fallback hash
|
||
let h = 0;
|
||
for (let i = 0; i < text.length; i++) h = (h * 31 + text.charCodeAt(i)) | 0;
|
||
return String(h);
|
||
}
|
||
}
|
||
|
||
function getMetadataCacheKey(contentHash: string, title?: string): string {
|
||
return `seo_metadata_cache:${contentHash}:${title || ''}`;
|
||
}
|
||
|
||
export const SEOMetadataModal: React.FC<SEOMetadataModalProps> = ({
|
||
isOpen,
|
||
onClose,
|
||
blogContent,
|
||
blogTitle,
|
||
researchData,
|
||
outline,
|
||
seoAnalysis,
|
||
onMetadataGenerated
|
||
}) => {
|
||
const [isGenerating, setIsGenerating] = useState(false);
|
||
const [metadataResult, setMetadataResult] = useState<SEOMetadataResult | null>(null);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [tabValue, setTabValue] = useState('preview'); // Start with preview tab first
|
||
const [previewTabValue, setPreviewTabValue] = useState('google'); // Sub-tab for preview platforms
|
||
const [copiedItems, setCopiedItems] = useState<Set<string>>(new Set());
|
||
const [editableMetadata, setEditableMetadata] = useState<SEOMetadataResult | null>(null);
|
||
const [contentHash, setContentHash] = useState<string>('');
|
||
// Subscribe to image generation bus to auto-fill OG/Twitter image fields
|
||
useEffect(() => {
|
||
const unsub = subscribeImage(({ base64 }: { base64: string }) => {
|
||
setEditableMetadata(prev => {
|
||
const next = { ...(prev || metadataResult || {}) } as any;
|
||
next.open_graph = { ...(next.open_graph || {}), image: `data:image/png;base64,${base64}` };
|
||
next.twitter_card = { ...(next.twitter_card || {}), image: `data:image/png;base64,${base64}` };
|
||
return next;
|
||
});
|
||
});
|
||
return unsub;
|
||
}, [metadataResult]);
|
||
|
||
// Debug logging only in development and when modal state changes meaningfully
|
||
useEffect(() => {
|
||
if (process.env.NODE_ENV === 'development' && isOpen) {
|
||
console.log('🔍 SEOMetadataModal render:', {
|
||
isOpen,
|
||
blogContent: blogContent?.length,
|
||
blogTitle,
|
||
researchData: !!researchData
|
||
});
|
||
}
|
||
}, [isOpen, blogContent?.length, blogTitle, researchData]);
|
||
|
||
// Reset state when modal closes
|
||
useEffect(() => {
|
||
if (!isOpen) {
|
||
// Reset state when modal closes (but keep result for next time)
|
||
setError(null);
|
||
setIsGenerating(false);
|
||
}
|
||
}, [isOpen]);
|
||
|
||
const generateMetadata = useCallback(async (forceRefresh = false) => {
|
||
try {
|
||
setIsGenerating(true);
|
||
setError(null);
|
||
if (forceRefresh) {
|
||
setMetadataResult(null);
|
||
}
|
||
|
||
console.log('🚀 Starting SEO metadata generation...', { forceRefresh });
|
||
|
||
// Calculate content hash for caching - use existing hash if available
|
||
let hash = contentHash;
|
||
if (!hash) {
|
||
hash = await hashContent(`${blogTitle || ''}\n${blogContent}`);
|
||
// Update state for future use
|
||
setContentHash(hash);
|
||
}
|
||
const cacheKey = getMetadataCacheKey(hash, blogTitle);
|
||
console.log('🔍 Checking SEO metadata cache', { cacheKey, hasHash: !!hash, forceRefresh });
|
||
|
||
// Check cache first (unless force refresh)
|
||
if (!forceRefresh && typeof window !== 'undefined') {
|
||
const cached = window.localStorage.getItem(cacheKey);
|
||
if (cached) {
|
||
try {
|
||
const parsed = JSON.parse(cached) as SEOMetadataResult;
|
||
// Validate cached data has required fields
|
||
if (parsed && parsed.success !== undefined) {
|
||
console.log('✅ Using cached SEO metadata', { cacheKey, success: parsed.success });
|
||
setMetadataResult(parsed);
|
||
setEditableMetadata(parsed);
|
||
setIsGenerating(false);
|
||
// Notify parent that metadata is available
|
||
if (onMetadataGenerated) {
|
||
onMetadataGenerated(parsed);
|
||
}
|
||
return;
|
||
} else {
|
||
console.warn('⚠️ Cached SEO metadata data is invalid, will fetch fresh metadata');
|
||
}
|
||
} catch (e) {
|
||
console.warn('⚠️ Failed to parse cached SEO metadata, will fetch fresh metadata', e);
|
||
// Remove invalid cache entry
|
||
if (typeof window !== 'undefined') {
|
||
window.localStorage.removeItem(cacheKey);
|
||
}
|
||
}
|
||
} else {
|
||
console.log('ℹ️ No cached SEO metadata found, will fetch from API', { cacheKey });
|
||
}
|
||
} else {
|
||
console.log('🔄 Force refresh requested, skipping cache check');
|
||
}
|
||
|
||
// Make API call to generate metadata
|
||
const response = await apiClient.post('/api/blog/seo/metadata', {
|
||
content: blogContent,
|
||
title: blogTitle,
|
||
research_data: researchData,
|
||
outline: outline || null,
|
||
seo_analysis: seoAnalysis || null
|
||
});
|
||
|
||
const result = response.data;
|
||
console.log('✅ SEO metadata generation response:', result);
|
||
|
||
// Check if the response indicates a subscription/usage error (even if HTTP status is 200)
|
||
if (!result.success && result.error) {
|
||
const errorMessage = (result.error || '').toLowerCase();
|
||
// Check if error message indicates subscription/balance limit
|
||
if (errorMessage.includes('token limit') ||
|
||
errorMessage.includes('balance') ||
|
||
errorMessage.includes('insufficient') ||
|
||
errorMessage.includes('limit would be exceeded') ||
|
||
errorMessage.includes('usage limit') ||
|
||
errorMessage.includes('subscription') ||
|
||
errorMessage.includes('403') ||
|
||
errorMessage.includes('429') ||
|
||
errorMessage.includes('quota')) {
|
||
console.log('SEOMetadataModal: Detected subscription error in response data', {
|
||
error: errorMessage,
|
||
data: result
|
||
});
|
||
|
||
// Create a mock error object with subscription error data
|
||
const mockError = {
|
||
response: {
|
||
status: 429, // Treat as 429 for subscription error
|
||
data: {
|
||
error: errorMessage,
|
||
message: result.message || errorMessage,
|
||
provider: result.provider || 'unknown',
|
||
usage_info: result.usage_info || {}
|
||
}
|
||
}
|
||
};
|
||
|
||
const handled = await triggerSubscriptionError(mockError);
|
||
if (handled) {
|
||
console.log('SEOMetadataModal: Global subscription error handler triggered successfully');
|
||
setIsGenerating(false);
|
||
return;
|
||
} else {
|
||
console.warn('SEOMetadataModal: Global subscription error handler did not handle the error');
|
||
}
|
||
}
|
||
|
||
// If not a subscription error, throw the error normally
|
||
throw new Error(result.error || 'Metadata generation failed');
|
||
}
|
||
|
||
// Cache the result
|
||
if (typeof window !== 'undefined') {
|
||
try {
|
||
window.localStorage.setItem(cacheKey, JSON.stringify(result));
|
||
console.log('💾 SEO metadata cached');
|
||
} catch (e) {
|
||
console.warn('Failed to cache metadata:', e);
|
||
}
|
||
}
|
||
|
||
const sanitizeMetadata = (data: any) => {
|
||
const safe = { ...data };
|
||
safe.seo_title = safe.seo_title ?? '';
|
||
safe.meta_description = safe.meta_description ?? '';
|
||
safe.url_slug = safe.url_slug ?? '';
|
||
safe.focus_keyword = safe.focus_keyword ?? '';
|
||
safe.reading_time = typeof safe.reading_time === 'number' ? safe.reading_time : 0;
|
||
safe.blog_tags = Array.isArray(safe.blog_tags) ? safe.blog_tags : [];
|
||
safe.blog_categories = Array.isArray(safe.blog_categories) ? safe.blog_categories : [];
|
||
safe.social_hashtags = Array.isArray(safe.social_hashtags) ? safe.social_hashtags : [];
|
||
safe.open_graph = {
|
||
...(safe.open_graph || {}),
|
||
title: safe.open_graph?.title ?? '',
|
||
description: safe.open_graph?.description ?? '',
|
||
image: safe.open_graph?.image ?? '',
|
||
url: safe.open_graph?.url ?? ''
|
||
};
|
||
safe.twitter_card = {
|
||
...(safe.twitter_card || {}),
|
||
title: safe.twitter_card?.title ?? '',
|
||
description: safe.twitter_card?.description ?? '',
|
||
image: safe.twitter_card?.image ?? '',
|
||
site: safe.twitter_card?.site ?? ''
|
||
};
|
||
safe.json_ld_schema = { ...(safe.json_ld_schema || {}) };
|
||
return safe;
|
||
};
|
||
const sanitized = sanitizeMetadata(result);
|
||
setMetadataResult(sanitized);
|
||
setEditableMetadata(sanitized);
|
||
console.log('📊 Metadata result set:', result);
|
||
|
||
} catch (err: any) {
|
||
console.error('❌ SEO metadata generation failed:', err);
|
||
|
||
// Check if this is a subscription error (429/402/403) or balance/limit issue
|
||
const status = err?.response?.status;
|
||
const rawError = err?.response?.data?.error || err?.response?.data?.message || '';
|
||
const errorMessage = err?.message || rawError || '';
|
||
const fullMessage = (errorMessage + ' ' + rawError + ' ' + JSON.stringify(err?.response?.data || {})).toLowerCase();
|
||
|
||
// Check HTTP status code for subscription/balance errors
|
||
if (status === 429 || status === 402 || status === 403) {
|
||
console.log('SEOMetadataModal: Detected usage/subscription error (HTTP status)', {
|
||
status,
|
||
data: err?.response?.data
|
||
});
|
||
const handled = await triggerSubscriptionError(err);
|
||
if (handled) {
|
||
console.log('SEOMetadataModal: Global subscription error handler triggered successfully');
|
||
setIsGenerating(false);
|
||
return;
|
||
} else {
|
||
console.warn('SEOMetadataModal: Global subscription error handler did not handle the error');
|
||
}
|
||
}
|
||
|
||
// Check error message for balance/usage/subscription-related errors
|
||
if (fullMessage.includes('balance') ||
|
||
fullMessage.includes('insufficient') ||
|
||
fullMessage.includes('limit would be exceeded') ||
|
||
fullMessage.includes('usage limit') ||
|
||
fullMessage.includes('token limit') ||
|
||
fullMessage.includes('subscription') ||
|
||
fullMessage.includes('429') ||
|
||
fullMessage.includes('403') ||
|
||
fullMessage.includes('quota')) {
|
||
console.log('SEOMetadataModal: Detected usage/subscription error (message match)', {
|
||
fullMessage,
|
||
err
|
||
});
|
||
|
||
const mockError = {
|
||
response: {
|
||
status: 429,
|
||
data: {
|
||
error: errorMessage,
|
||
message: errorMessage,
|
||
provider: err?.response?.data?.provider || 'unknown',
|
||
usage_info: err?.response?.data?.usage_info || {}
|
||
}
|
||
}
|
||
};
|
||
|
||
const handled = await triggerSubscriptionError(mockError);
|
||
if (handled) {
|
||
console.log('SEOMetadataModal: Global subscription error handler triggered successfully (from message)');
|
||
setIsGenerating(false);
|
||
return;
|
||
} else {
|
||
console.warn('SEOMetadataModal: Global subscription error handler did not handle the error');
|
||
}
|
||
}
|
||
|
||
// For non-subscription errors, show local error message
|
||
setError(err instanceof Error ? err.message : 'Failed to generate SEO metadata');
|
||
setIsGenerating(false);
|
||
}
|
||
}, [blogContent, blogTitle, researchData, outline, seoAnalysis, contentHash, onMetadataGenerated]);
|
||
|
||
// Precompute hash when modal opens and trigger cache check
|
||
useEffect(() => {
|
||
if (isOpen) {
|
||
(async () => {
|
||
const h = await hashContent(`${blogTitle || ''}\n${blogContent}`);
|
||
setContentHash(h);
|
||
// After hash is computed, check cache if we don't have metadata result yet
|
||
if (!metadataResult) {
|
||
// Small delay to ensure hash is set in state
|
||
setTimeout(() => {
|
||
generateMetadata(false);
|
||
}, 100);
|
||
}
|
||
})();
|
||
} else {
|
||
// Reset hash when modal closes
|
||
setContentHash('');
|
||
}
|
||
}, [isOpen, blogContent, blogTitle, metadataResult, generateMetadata]);
|
||
|
||
// Fallback: if modal opens and hash is already computed, check cache immediately
|
||
useEffect(() => {
|
||
if (isOpen && !metadataResult && contentHash) {
|
||
generateMetadata(false);
|
||
}
|
||
}, [isOpen, metadataResult, contentHash, generateMetadata]);
|
||
|
||
const handleTabChange = (event: React.SyntheticEvent, newValue: string) => {
|
||
setTabValue(newValue);
|
||
};
|
||
|
||
const handleCopyToClipboard = async (text: string, itemId: string) => {
|
||
try {
|
||
await navigator.clipboard.writeText(text);
|
||
setCopiedItems(prev => new Set([...prev, itemId]));
|
||
setTimeout(() => {
|
||
setCopiedItems(prev => {
|
||
const newSet = new Set(prev);
|
||
newSet.delete(itemId);
|
||
return newSet;
|
||
});
|
||
}, 2000);
|
||
} catch (err) {
|
||
console.error('Failed to copy to clipboard:', err);
|
||
}
|
||
};
|
||
|
||
const handleMetadataEdit = (field: string, value: any) => {
|
||
if (editableMetadata) {
|
||
setEditableMetadata(prev => ({
|
||
...prev!,
|
||
[field]: value
|
||
}));
|
||
}
|
||
};
|
||
|
||
/**
|
||
* Handle Apply Metadata button click
|
||
*
|
||
* This saves the generated/edited metadata to the parent component's state.
|
||
* The metadata is then used when publishing to platforms:
|
||
* - WordPress: Requires SEO metadata for proper post creation with SEO fields
|
||
* - Wix: Currently doesn't require metadata, but could be added in future
|
||
*
|
||
* The metadata includes:
|
||
* - SEO title, meta description, URL slug
|
||
* - Blog tags, categories, focus keyword
|
||
* - Open Graph tags (Facebook/LinkedIn)
|
||
* - Twitter Card tags
|
||
* - JSON-LD structured data (Schema.org Article)
|
||
*
|
||
* All of these will be passed to the platform's API when publishing.
|
||
*/
|
||
const handleApplyMetadata = () => {
|
||
if (editableMetadata) {
|
||
onMetadataGenerated(editableMetadata);
|
||
onClose();
|
||
}
|
||
};
|
||
|
||
const getTabIcon = (tabValue: string) => {
|
||
switch (tabValue) {
|
||
case 'core': return <SearchIcon />;
|
||
case 'social': return <ShareIcon />;
|
||
case 'structured': return <CodeIcon />;
|
||
case 'preview': return <PreviewIcon />;
|
||
default: return <TagIcon />;
|
||
}
|
||
};
|
||
|
||
const getTabLabel = (tabValue: string) => {
|
||
switch (tabValue) {
|
||
case 'core': return 'Core SEO';
|
||
case 'social': return 'Social Media';
|
||
case 'structured': return 'Structured Data';
|
||
case 'preview': return 'Preview';
|
||
default: return 'Metadata';
|
||
}
|
||
};
|
||
|
||
return (
|
||
<Dialog
|
||
open={isOpen}
|
||
onClose={onClose}
|
||
maxWidth="lg"
|
||
fullWidth
|
||
PaperProps={{
|
||
sx: {
|
||
background: 'rgba(255, 255, 255, 0.98)',
|
||
backdropFilter: 'blur(10px)',
|
||
borderRadius: 3,
|
||
minHeight: '80vh'
|
||
}
|
||
}}
|
||
>
|
||
<DialogTitle sx={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
pb: 1,
|
||
borderBottom: '1px solid rgba(0,0,0,0.1)'
|
||
}}>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||
<TagIcon sx={{ color: 'primary.main' }} />
|
||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||
SEO Metadata Generator
|
||
</Typography>
|
||
{metadataResult && (
|
||
<Chip
|
||
label={`${metadataResult.optimization_score || 0}% Optimized`}
|
||
color={metadataResult.optimization_score && metadataResult.optimization_score >= 80 ? 'success' :
|
||
metadataResult.optimization_score && metadataResult.optimization_score >= 60 ? 'warning' : 'error'}
|
||
size="small"
|
||
/>
|
||
)}
|
||
</Box>
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||
{metadataResult && (
|
||
<Tooltip title="Regenerate SEO metadata">
|
||
<IconButton
|
||
onClick={() => generateMetadata(true)}
|
||
size="small"
|
||
disabled={isGenerating}
|
||
color="primary"
|
||
>
|
||
<RefreshIcon />
|
||
</IconButton>
|
||
</Tooltip>
|
||
)}
|
||
<IconButton onClick={onClose} size="small">
|
||
<CloseIcon />
|
||
</IconButton>
|
||
</Box>
|
||
</DialogTitle>
|
||
|
||
<DialogContent sx={{ p: 0 }}>
|
||
{isGenerating && (
|
||
<Box sx={{ p: 4, textAlign: 'center' }}>
|
||
<CircularProgress size={60} sx={{ mb: 2 }} />
|
||
<Typography variant="h6" sx={{ mb: 1 }}>
|
||
Generating SEO Metadata...
|
||
</Typography>
|
||
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
|
||
Creating optimized titles, descriptions, and social media tags
|
||
</Typography>
|
||
</Box>
|
||
)}
|
||
|
||
{error && (
|
||
<Box sx={{ p: 3 }}>
|
||
<Alert severity="error" sx={{ mb: 2 }}>
|
||
{error}
|
||
</Alert>
|
||
<Button
|
||
variant="outlined"
|
||
onClick={() => generateMetadata(true)}
|
||
startIcon={<RefreshIcon />}
|
||
>
|
||
Try Again
|
||
</Button>
|
||
</Box>
|
||
)}
|
||
|
||
{metadataResult && (
|
||
<Box>
|
||
{/* Tabs */}
|
||
<Box sx={{ borderBottom: 1, borderColor: 'divider', px: 3 }}>
|
||
<Tabs
|
||
value={tabValue}
|
||
onChange={handleTabChange}
|
||
variant="scrollable"
|
||
scrollButtons="auto"
|
||
sx={{ minHeight: 48 }}
|
||
>
|
||
{['preview', 'core', 'social', 'structured'].map((tab) => (
|
||
<Tab
|
||
key={tab}
|
||
value={tab}
|
||
label={getTabLabel(tab)}
|
||
icon={getTabIcon(tab)}
|
||
iconPosition="start"
|
||
sx={{ minHeight: 48, textTransform: 'none' }}
|
||
/>
|
||
))}
|
||
</Tabs>
|
||
</Box>
|
||
|
||
{/* Tab Content */}
|
||
<Box sx={{ p: 3 }}>
|
||
{tabValue === 'core' && (
|
||
<CoreMetadataTab
|
||
metadata={editableMetadata || metadataResult}
|
||
onMetadataEdit={handleMetadataEdit}
|
||
onCopyToClipboard={handleCopyToClipboard}
|
||
copiedItems={copiedItems}
|
||
/>
|
||
)}
|
||
|
||
{tabValue === 'social' && (
|
||
<SocialMediaTab
|
||
metadata={editableMetadata || metadataResult}
|
||
onMetadataEdit={handleMetadataEdit}
|
||
onCopyToClipboard={handleCopyToClipboard}
|
||
copiedItems={copiedItems}
|
||
/>
|
||
)}
|
||
|
||
{tabValue === 'structured' && (
|
||
<StructuredDataTab
|
||
metadata={editableMetadata || metadataResult}
|
||
onMetadataEdit={handleMetadataEdit}
|
||
onCopyToClipboard={handleCopyToClipboard}
|
||
copiedItems={copiedItems}
|
||
/>
|
||
)}
|
||
|
||
{tabValue === 'preview' && (
|
||
<PreviewCard
|
||
metadata={editableMetadata || metadataResult}
|
||
blogTitle={blogTitle}
|
||
previewTabValue={previewTabValue}
|
||
onPreviewTabChange={setPreviewTabValue}
|
||
/>
|
||
)}
|
||
</Box>
|
||
</Box>
|
||
)}
|
||
</DialogContent>
|
||
|
||
{metadataResult && (
|
||
<DialogActions sx={{ p: 3, borderTop: '1px solid rgba(0,0,0,0.1)' }}>
|
||
<Button onClick={onClose} color="inherit">
|
||
Cancel
|
||
</Button>
|
||
<Button
|
||
variant="contained"
|
||
onClick={handleApplyMetadata}
|
||
startIcon={<CheckIcon />}
|
||
sx={{ px: 3 }}
|
||
>
|
||
Apply Metadata
|
||
</Button>
|
||
</DialogActions>
|
||
)}
|
||
</Dialog>
|
||
);
|
||
};
|