"use client";
import { useState } from 'react';
import { Input, Textarea, Label, Button, Badge, Field } from '../ui';
import { ConfidenceBadge } from '../features';
import { Globe, ExternalLink, Upload, X, Loader2, Sparkles } from 'lucide-react';
import { AIGeneratedBadge } from '../ui/ai-generated-badge';
import { cn } from '../../utils';
import Image from '../../embed-shims/next-image';
// SSOT for the field cap (server-safe constant). The seo_title renders as the
// page
verbatim (no brand suffix), so this is the full ~60-char budget.
import { SEO_TITLE_MAX_LENGTH } from '../../utils/seo-title';
import { SEO_DESCRIPTION_MAX_LENGTH } from '../../utils/seo-description';
/** SEO Description textarea and the OG upload box sit side by side on desktop —
* ONE height constant for both, so the pair stays flush instead of each
* picking its own (`rows` on one, `min-h` on the other, drifting apart). */
const SEO_MEDIA_BOX_HEIGHT = 'h-[280px]'
export interface SEOEditorPreviewProps {
// SEO fields - must be strings (not undefined)
seoTitle: string;
seoDescription: string;
seoKeywords: string;
ogImageUrl: string;
// Auto-populate sources - must be strings (not undefined)
title: string;
summary: string;
featuredImage: string;
// Change handlers
onSeoTitleChange: (value: string) => void;
onSeoDescriptionChange: (value: string) => void;
onSeoKeywordsChange: (value: string) => void;
onOgImageUrlChange: (value: string) => void;
// Upload handler (provided by parent since it needs API endpoint)
onOgImageUpload?: (file: File) => Promise;
// AI confidence scores
aiConfidenceSeoTitle?: number;
aiConfidenceSeoDescription?: number;
aiConfidenceSeoKeywords?: number;
// Optional
domain?: string;
disabled?: boolean;
className?: string;
}
/**
* Unified SEO Editor with Live Preview
* Complete self-contained component for managing SEO meta tags and OG preview
* Used across blog posts, case studies, and product releases
*/
export function SEOEditorPreview({
seoTitle,
seoDescription,
seoKeywords,
ogImageUrl,
title,
summary,
featuredImage,
onSeoTitleChange,
onSeoDescriptionChange,
onSeoKeywordsChange,
onOgImageUrlChange,
onOgImageUpload,
aiConfidenceSeoTitle,
aiConfidenceSeoDescription,
aiConfidenceSeoKeywords,
domain = 'openmsp.ai',
disabled = false,
className = ''
}: SEOEditorPreviewProps) {
const [imageError, setImageError] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [fileInputRef, setFileInputRef] = useState(null);
// SEO title length state — alerts the editor when the title would render too long.
const seoTitleLength = (seoTitle || '').length;
const seoTitleTooLong = seoTitleLength > SEO_TITLE_MAX_LENGTH;
// SEO description length state — the column is varchar(160); flag over-length so
// the editor (or a programmatically-set value) can be brought within budget.
const seoDescriptionLength = (seoDescription || '').length;
const seoDescriptionTooLong = seoDescriptionLength > SEO_DESCRIPTION_MAX_LENGTH;
// Use fallback values if OG fields are empty
const displayTitle = seoTitle.trim() || title || 'Untitled';
const displayDescription = seoDescription.trim() || summary || 'No description';
const hasOgImage = ogImageUrl.trim();
const hasFeaturedImage = featuredImage.trim();
const displayImage = hasOgImage || hasFeaturedImage;
const handleImageUpload = async (event: React.ChangeEvent) => {
if (!onOgImageUpload) return;
const file = event.target.files?.[0];
if (!file) return;
setIsUploading(true);
try {
const url = await onOgImageUpload(file);
onOgImageUrlChange(url);
} catch (error) {
console.error('OG image upload failed:', error);
} finally {
setIsUploading(false);
}
};
return (
SEO & Open Graph
{/* SEO Title & Keywords - Same Row. EVERY field goes through `Field`, so
both columns share one label-row geometry; badges + the char counter
ride the label row (labelExtras / labelEnd) instead of adding stray
rows that misalign siblings. */}
>
) : undefined
}
labelEnd={
{seoTitleLength}/{SEO_TITLE_MAX_LENGTH}
}
>
{(f) => (
onSeoTitleChange(e.target.value)}
disabled={disabled}
maxLength={SEO_TITLE_MAX_LENGTH}
invalid={seoTitleTooLong}
placeholder="Enter SEO meta title..."
className="bg-ods-bg border-ods-border text-ods-text-primary"
/>
)}
{!seoTitle && title && (
{/* Group label for the upload widget — Field's exact label treatment,
so the OG column's label row aligns with the Description Field. */}
{/* OG Image Upload/Display — pt matches Field's label→control offset
so this box's top aligns with the Description textarea's top. */}