357 lines
24 KiB
TypeScript
357 lines
24 KiB
TypeScript
'use client';
|
|
import { useState, useEffect } from 'react';
|
|
import axios from 'axios';
|
|
import Link from 'next/link';
|
|
import IconArrowWaveLeftUp from '@/components/icon/icon-arrow-wave-left-up';
|
|
import IconRefresh from '@/components/icon/icon-refresh';
|
|
import IconCopy from '@/components/icon/icon-copy';
|
|
import IconDownload from '@/components/icon/icon-download';
|
|
import Loading from '@/components/layouts/loading';
|
|
import ReactMarkdown from 'react-markdown';
|
|
import remarkGfm from 'remark-gfm';
|
|
|
|
const BlogGenerator = () => {
|
|
const [url, setUrl] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [blogData, setBlogData] = useState<any>(null);
|
|
const [error, setError] = useState('');
|
|
const [recentBlogs, setRecentBlogs] = useState<any[]>([]);
|
|
const [selectedEngine, setEngine] = useState('seomagnifier');
|
|
|
|
useEffect(() => {
|
|
fetchRecentBlogs();
|
|
}, [url]);
|
|
|
|
const fetchRecentBlogs = async () => {
|
|
try {
|
|
const { data } = await axios.get(`http://localhost:3020/api/blog/recent${url ? `?url=${encodeURIComponent(url)}` : ''}`);
|
|
if (data.ok) {
|
|
setRecentBlogs(data.data);
|
|
}
|
|
} catch (err) {
|
|
console.error('Error fetching recent blogs:', err);
|
|
}
|
|
};
|
|
|
|
const handleGenerate = async () => {
|
|
if (!url) return setError('Please enter a website URL');
|
|
|
|
setLoading(true);
|
|
setError('');
|
|
setBlogData(null);
|
|
|
|
try {
|
|
const { data } = await axios.post('http://localhost:3020/api/blog/generate', {
|
|
url,
|
|
engine: selectedEngine
|
|
});
|
|
if (data.ok) {
|
|
setBlogData(data.data);
|
|
fetchRecentBlogs();
|
|
} else {
|
|
setError('Failed to generate content');
|
|
}
|
|
} catch (err: any) {
|
|
setError(err.response?.data?.error || err.message || 'Something went wrong');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const copyToClipboard = (text: string) => {
|
|
navigator.clipboard.writeText(text);
|
|
alert('Copied to clipboard!');
|
|
};
|
|
|
|
return (
|
|
<div className="pb-10 lg:flex gap-6 max-w-[1600px] mx-auto">
|
|
<div className="flex-1 min-w-0">
|
|
{/* Header Section */}
|
|
<div className="relative rounded-t-md bg-primary-light bg-[url('/assets/images/knowledge/pattern.png')] bg-contain bg-left-top bg-no-repeat px-5 py-10 dark:bg-black md:px-10">
|
|
<div className="relative z-[1]">
|
|
<div className="flex flex-col items-center justify-center sm:-ms-32 sm:flex-row xl:-ms-60">
|
|
<div className="mb-2 flex gap-1 text-end text-base leading-5 sm:flex-col xl:text-xl">
|
|
<span>One Click</span>
|
|
<span>Expert Content</span>
|
|
</div>
|
|
<div className="me-4 ms-2 hidden text-[#0E1726] rtl:rotate-y-180 dark:text-white sm:block">
|
|
<IconArrowWaveLeftUp className="w-16 xl:w-28" />
|
|
</div>
|
|
<div className="mb-2 text-center text-2xl font-bold dark:text-white md:text-5xl">AI Blog Generator</div>
|
|
</div>
|
|
<p className="mb-9 text-center text-base font-semibold">Transform any website URL into an SEO-optimized, human-readable blog post in seconds.</p>
|
|
|
|
<div className="relative mx-auto max-w-[700px] space-y-4">
|
|
<div className="relative">
|
|
<input
|
|
type="text"
|
|
value={url}
|
|
onChange={(e) => setUrl(e.target.value)}
|
|
placeholder="Enter Website URL (e.g., https://example.com)"
|
|
className="form-input py-4 ltr:pr-[120px] rtl:pl-[120px] shadow-lg rounded-full border-2 border-primary/20 focus:border-primary transition-all text-lg"
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={handleGenerate}
|
|
disabled={loading}
|
|
className={`btn btn-primary absolute top-1.5 ltr:right-1.5 rtl:left-1.5 rounded-full py-2.5 px-6 shadow-none flex items-center gap-2 ${loading ? 'bg-gray-400 opacity-70' : 'bg-primary'}`}
|
|
>
|
|
{loading ? (
|
|
<>
|
|
<span className="animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full"></span>
|
|
Analyzing...
|
|
</>
|
|
) : (
|
|
'Generate Blog'
|
|
)}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap items-center justify-center gap-4 animate-fade-in">
|
|
<span className="text-xs font-bold uppercase text-gray-400 tracking-wider">AI Engine:</span>
|
|
<div className="flex bg-gray-100 dark:bg-dark p-1 rounded-xl border border-gray-200 dark:border-gray-800">
|
|
{[
|
|
{ id: 'seomagnifier', name: 'SEO Magnifier', icon: '🆓', sub: 'Unlimited' },
|
|
{ id: 'ollama', name: 'Ollama', icon: '🏠', sub: 'Local' },
|
|
{ id: 'openai', name: 'OpenAI', icon: '💎', sub: 'Premium' },
|
|
].map((engine) => (
|
|
<button
|
|
key={engine.id}
|
|
onClick={() => setEngine(engine.id)}
|
|
className={`flex flex-col items-center px-4 py-2 rounded-lg transition-all ${selectedEngine === engine.id
|
|
? 'bg-white dark:bg-gray-700 shadow-md text-primary scale-105'
|
|
: 'text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'
|
|
}`}
|
|
>
|
|
<span className="text-lg">{engine.icon}</span>
|
|
<span className="text-[10px] font-bold uppercase">{engine.name}</span>
|
|
<span className="text-[8px] opacity-70">{engine.sub}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{error && <p className="text-red-500 mt-4 text-center font-medium animate-bounce">{error}</p>}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="px-5 md:px-10 mt-10">
|
|
{!blogData && !loading && (
|
|
<div className="flex flex-col items-center justify-center py-20 text-center opacity-40">
|
|
<div className="bg-gray-200 p-6 rounded-full mb-4">
|
|
<IconRefresh className="w-12 h-12" />
|
|
</div>
|
|
<p className="text-xl font-medium">Enter a URL above to start the magic.</p>
|
|
<p className="text-sm">We'll crawl the site, analyze its SEO structure, and build a custom blog for you.</p>
|
|
</div>
|
|
)}
|
|
|
|
{loading && (
|
|
<div className="flex flex-col items-center justify-center py-20">
|
|
<div className="loader ring-4 ring-primary border-4 border-transparent border-t-primary w-16 h-16 rounded-full animate-spin"></div>
|
|
<p className="mt-6 text-xl font-bold animate-pulse">Deep Auditing Website Structure...</p>
|
|
<p className="text-gray-500">Extracting semantic markers and entity relationships...</p>
|
|
</div>
|
|
)}
|
|
|
|
{blogData && (
|
|
<div className="grid grid-cols-1 xl:grid-cols-12 gap-6">
|
|
{/* Main Blog Content Section */}
|
|
<div className="xl:col-span-8 space-y-6">
|
|
<div className="panel shadow-xl border-t-4 border-primary">
|
|
<div className="flex items-center justify-between mb-6 pb-4 border-b">
|
|
<h2 className="text-2xl font-bold text-gray-800 dark:text-white-light">Generated Blog Post</h2>
|
|
<div className="flex gap-2">
|
|
<Link href={`/blog/${blogData._id}`} className="btn btn-primary btn-sm px-3 py-2 flex items-center gap-2">
|
|
View Full View
|
|
</Link>
|
|
<button onClick={() => copyToClipboard(blogData.content)} className="btn btn-outline-primary btn-sm px-3 py-2 flex items-center gap-2">
|
|
<IconCopy className="w-4 h-4" /> Copy Content
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="prose prose-blue dark:prose-invert max-w-none">
|
|
<div className="mb-8 p-4 bg-gray-50 dark:bg-dark rounded-lg border-s-4 border-primary italic text-gray-600 dark:text-gray-400">
|
|
<p className="font-bold text-primary mb-1">SEO Blueprint Info:</p>
|
|
<ul className="text-sm space-y-1">
|
|
<li><strong>Meta Title:</strong> {blogData.metaTitle}</li>
|
|
<li><strong>Meta Description:</strong> {blogData.metaDescription}</li>
|
|
<li><strong>Focus Keyword:</strong> {blogData.focusKeyword}</li>
|
|
</ul>
|
|
</div>
|
|
|
|
{blogData.generatedImageUrls?.[0] && (
|
|
<div className="mb-8 rounded-2xl overflow-hidden shadow-2xl relative group">
|
|
<img src={blogData.generatedImageUrls[0]} alt="Generated blog header" className="w-full h-[400px] object-cover transition-transform duration-500 group-hover:scale-105" />
|
|
<div className="absolute top-4 right-4 badge badge-primary bg-primary/80 backdrop-blur-md border-none">AI Generated</div>
|
|
</div>
|
|
)}
|
|
|
|
<h1 className="text-4xl lg:text-5xl font-extrabold mb-10 leading-tight text-black dark:text-white">{blogData.title}</h1>
|
|
<div className="prose prose-lg dark:prose-invert max-w-none
|
|
prose-p:text-lg prose-p:leading-relaxed prose-p:text-gray-700 dark:prose-p:text-gray-300
|
|
prose-h2:text-3xl prose-h2:mt-12 prose-h2:mb-6 prose-h2:font-bold
|
|
prose-h3:text-2xl prose-h3:mt-8 prose-h3:font-bold
|
|
prose-blockquote:border-primary prose-blockquote:bg-primary/5 prose-blockquote:py-4 prose-blockquote:px-8 prose-blockquote:rounded-r-2xl prose-blockquote:italic">
|
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
|
{blogData.content}
|
|
</ReactMarkdown>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Image Prompts Section */}
|
|
<div className="panel shadow-lg">
|
|
<h3 className="text-xl font-bold mb-4 flex items-center gap-2">
|
|
<span className="p-2 bg-secondary/10 text-secondary rounded">
|
|
🎨
|
|
</span>
|
|
AI Image Prompts
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
{blogData.imagePrompts.map((prompt: string, i: number) => (
|
|
<div key={i} className="p-4 bg-gray-50 dark:bg-dark rounded-xl border border-gray-200 dark:border-gray-800 relative group">
|
|
<p className="text-sm italic text-gray-700 dark:text-gray-300">"{prompt}"</p>
|
|
<button
|
|
onClick={() => copyToClipboard(prompt)}
|
|
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity bg-white dark:bg-gray-800 p-1 rounded shadow"
|
|
title="Copy prompt"
|
|
>
|
|
<IconCopy className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Sidebar Metrics/Analysis Section */}
|
|
<div className="xl:col-span-4 space-y-6">
|
|
<div className="panel shadow-lg bg-gradient-to-br from-primary/5 to-transparent">
|
|
<h3 className="text-xl font-bold mb-4">Content Quality</h3>
|
|
<div className="space-y-6">
|
|
<div>
|
|
<div className="flex justify-between mb-2">
|
|
<span className="font-semibold">SEO Optimization</span>
|
|
<span className="text-primary font-bold">{blogData.seoScore}%</span>
|
|
</div>
|
|
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
|
<div className="bg-primary h-2.5 rounded-full shadow-[0_0_10px_rgba(67,97,238,0.5)] transition-all duration-1000" style={{ width: `${blogData.seoScore}%` }}></div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="flex justify-between mb-2">
|
|
<span className="font-semibold">AI Detection (Human Score)</span>
|
|
<span className="text-info font-bold">{blogData.aiDetectionScore || 92}%</span>
|
|
</div>
|
|
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
|
<div className="bg-info h-2.5 rounded-full shadow-[0_0_10px_rgba(0,186,211,0.5)] transition-all duration-1000" style={{ width: `${blogData.aiDetectionScore || 92}%` }}></div>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<div className="flex justify-between mb-2">
|
|
<span className="font-semibold">Human Readability</span>
|
|
<span className="text-secondary font-bold">{blogData.readabilityScore}%</span>
|
|
</div>
|
|
<div className="w-full bg-gray-200 rounded-full h-2.5">
|
|
<div className="bg-secondary h-2.5 rounded-full shadow-[0_0_10px_rgba(128,94,255,0.5)] transition-all duration-1000" style={{ width: `${blogData.readabilityScore}%` }}></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="pt-4 mt-2 border-t border-dashed border-gray-200 dark:border-gray-700">
|
|
<div className="flex justify-between items-center bg-warning/10 p-3 rounded-lg border border-warning/20">
|
|
<span className="font-black text-[10px] uppercase text-warning-dark tracking-widest">Focus Keyword</span>
|
|
<span className="text-xs font-bold text-warning-dark truncate max-w-[150px]">{blogData.focusKeyword || 'AI Optimized'}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{blogData.seoDetails && (
|
|
<div className="mt-4 pt-4 border-t border-gray-100 dark:border-gray-800">
|
|
<h4 className="text-xs font-bold text-gray-500 uppercase mb-3">SEO Checklist</h4>
|
|
<ul className="space-y-2">
|
|
{blogData.seoDetails.map((detail: string, i: number) => (
|
|
<li key={i} className="text-[11px] flex items-center gap-2">
|
|
<span className={`w-1.5 h-1.5 rounded-full ${detail.toLowerCase().includes('missing') ? 'bg-red-500' : 'bg-green-500'}`}></span>
|
|
<span className={detail.toLowerCase().includes('missing') ? 'text-red-500' : 'text-gray-600 dark:text-gray-400 font-medium'}>{detail}</span>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="panel shadow-lg">
|
|
<h3 className="text-xl font-bold mb-4">Internal Link Strategy</h3>
|
|
<p className="text-sm text-gray-500 mb-4">We've identified the high-authority endpoints to link from this blog post:</p>
|
|
<div className="space-y-3">
|
|
<div className="p-3 bg-gray-50 dark:bg-dark rounded border-l-2 border-primary">
|
|
<p className="text-xs font-bold text-primary uppercase">Primary Anchor</p>
|
|
<p className="text-sm font-medium">Link to "Solutions" page</p>
|
|
</div>
|
|
<div className="p-3 bg-gray-50 dark:bg-dark rounded border-l-2 border-secondary">
|
|
<p className="text-xs font-bold text-secondary uppercase">Secondary Anchor</p>
|
|
<p className="text-sm font-medium">Link to "Our Process" deep dive</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="panel bg-black text-white p-6 rounded-2xl shadow-2xl relative overflow-hidden group">
|
|
<div className="absolute -top-10 -right-10 w-40 h-40 bg-primary/20 rounded-full blur-3xl group-hover:scale-150 transition-all duration-700"></div>
|
|
<h4 className="text-2xl font-bold mb-4 relative z-[1]">Power Up Your Content</h4>
|
|
<p className="text-gray-400 mb-6 relative z-[1]">Upgrade to Pro to unlock unlimited crawls and 100% genuine Gemini-powered content.</p>
|
|
<button className="btn btn-primary w-full py-4 text-lg font-bold relative z-[1] shadow-[0_0_20px_rgba(67,97,238,0.3)]">
|
|
Upgrade Now
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Recently Generated Blogs Sidebar */}
|
|
<div className="lg:w-[350px] shrink-0 space-y-6 mt-10 lg:mt-0 px-5 lg:px-0">
|
|
<div className="panel shadow-lg h-fit sticky top-24">
|
|
<div className="flex items-center justify-between mb-4 pb-2 border-b">
|
|
<h3 className="text-xl font-bold">Recent Blogs</h3>
|
|
<button type="button" onClick={fetchRecentBlogs} className="hover:rotate-180 transition-all duration-500 text-primary">
|
|
<IconRefresh className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
{recentBlogs.length === 0 ? (
|
|
<p className="text-sm text-gray-500 text-center py-10">No blogs generated yet {url ? 'for this URL' : ''}.</p>
|
|
) : (
|
|
<div className="space-y-4 max-h-[70vh] overflow-y-auto pr-2 custom-scrollbar">
|
|
{recentBlogs.map((blog) => (
|
|
<div
|
|
key={blog._id}
|
|
className={`p-3 rounded-lg border cursor-pointer transition-all hover:border-primary group ${blogData?._id === blog._id ? 'bg-primary/5 border-primary' : 'bg-gray-50 dark:bg-dark border-transparent'}`}
|
|
onClick={() => setBlogData(blog)}
|
|
>
|
|
<h4 className="font-bold text-sm line-clamp-2 group-hover:text-primary transition-colors">{blog.title}</h4>
|
|
<div className="flex items-center justify-between mt-2">
|
|
<span className="text-[10px] text-gray-400">{new Date(blog.createdAt).toLocaleDateString()}</span>
|
|
<span className="text-[10px] px-2 py-0.5 bg-gray-200 dark:bg-gray-800 rounded-full font-medium">SEO: {blog.seoScore}%</span>
|
|
</div>
|
|
<p className="text-[10px] text-gray-500 mt-1 truncate">{blog.url}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
<div className="mt-6 pt-4 border-t">
|
|
<p className="text-[11px] text-gray-400 text-center">
|
|
History is synced to your account.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default BlogGenerator;
|