Add preview functionality and domain setup guide
- Created DomainSetupGuide component with 4-step wizard - DNS configuration instructions with specific records - Domain verification tool - Added preview routes: /preview/page/[id] and /preview/post/[id] - Preview routes show draft banner with copy link - Updated SiteList with preview buttons and domain status - Integrated DomainSetupGuide into SiteEditor - Fixed Site schema references (domain not url) Users can now preview content and get domain setup instructions.
This commit is contained in:
168
frontend/src/components/admin/DomainSetupGuide.tsx
Normal file
168
frontend/src/components/admin/DomainSetupGuide.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
interface DomainSetupGuideProps {
|
||||
siteDomain?: string;
|
||||
}
|
||||
|
||||
export default function DomainSetupGuide({ siteDomain }: DomainSetupGuideProps) {
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const [verifying, setVerifying] = useState(false);
|
||||
const [verificationStatus, setVerificationStatus] = useState<'pending' | 'success' | 'error'>('pending');
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: '1. Get Your Domain',
|
||||
content: (
|
||||
<div className="space-y-3">
|
||||
<p className="text-slate-300">Purchase a domain from any registrar:</p>
|
||||
<ul className="list-disc list-inside space-y-1 text-slate-400">
|
||||
<li><a href="https://www.namecheap.com" target="_blank" rel="noopener" className="text-blue-400 hover:underline">Namecheap</a> (Recommended)</li>
|
||||
<li><a href="https://www.godaddy.com" target="_blank" rel="noopener" className="text-blue-400 hover:underline">GoDaddy</a></li>
|
||||
<li><a href="https://www.cloudflare.com/products/registrar/" target="_blank" rel="noopener" className="text-blue-400 hover:underline">Cloudflare</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '2. Configure DNS Records',
|
||||
content: (
|
||||
<div className="space-y-3">
|
||||
<p className="text-slate-300">Add these DNS records at your registrar:</p>
|
||||
<div className="bg-slate-900 p-4 rounded border border-slate-700 font-mono text-sm space-y-2">
|
||||
<div>
|
||||
<div className="text-slate-500">Type: <span className="text-green-400">CNAME</span></div>
|
||||
<div className="text-slate-500">Name: <span className="text-blue-400">www</span></div>
|
||||
<div className="text-slate-500">Target: <span className="text-purple-400">launch.jumpstartscaling.com</span></div>
|
||||
</div>
|
||||
<div className="border-t border-slate-700 pt-2">
|
||||
<div className="text-slate-500">Type: <span className="text-green-400">A</span></div>
|
||||
<div className="text-slate-500">Name: <span className="text-blue-400">@</span> (root)</div>
|
||||
<div className="text-slate-500">Value: <span className="text-purple-400">72.61.15.216</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 italic">DNS propagation can take 5-60 minutes</p>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '3. Update Site Settings',
|
||||
content: (
|
||||
<div className="space-y-3">
|
||||
<p className="text-slate-300">Add your domain to Spark:</p>
|
||||
<ol className="list-decimal list-inside space-y-2 text-slate-400">
|
||||
<li>Go to Sites & Deployments</li>
|
||||
<li>Edit your site</li>
|
||||
<li>Set the "URL" field to your domain</li>
|
||||
<li>Save changes</li>
|
||||
</ol>
|
||||
{siteDomain && (
|
||||
<div className="bg-blue-900/20 border border-blue-700 rounded p-3">
|
||||
<p className="text-sm text-blue-300">
|
||||
Current domain: <span className="font-mono font-bold">{siteDomain}</span>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '4. Verify Connection',
|
||||
content: (
|
||||
<div className="space-y-3">
|
||||
<p className="text-slate-300">Test if your domain is connected:</p>
|
||||
<button
|
||||
onClick={async () => {
|
||||
if (!siteDomain) {
|
||||
alert('Please set a domain in your site settings first');
|
||||
return;
|
||||
}
|
||||
setVerifying(true);
|
||||
try {
|
||||
const response = await fetch(`https://${siteDomain}`, {
|
||||
method: 'HEAD',
|
||||
mode: 'no-cors'
|
||||
});
|
||||
setVerificationStatus('success');
|
||||
} catch (error) {
|
||||
setVerificationStatus('error');
|
||||
}
|
||||
setVerifying(false);
|
||||
}}
|
||||
disabled={!siteDomain || verifying}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
{verifying ? 'Checking...' : 'Verify Domain'}
|
||||
</button>
|
||||
|
||||
{verificationStatus === 'success' && (
|
||||
<div className="bg-green-900/20 border border-green-700 rounded p-3">
|
||||
<p className="text-green-300">✅ Domain connected successfully!</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{verificationStatus === 'error' && (
|
||||
<div className="bg-red-900/20 border border-red-700 rounded p-3 space-y-2">
|
||||
<p className="text-red-300">❌ Domain not reachable yet</p>
|
||||
<p className="text-sm text-red-400">DNS may still be propagating. Wait a few minutes and try again.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-blue-600 to-purple-600 p-4">
|
||||
<h3 className="text-xl font-bold text-white">🌐 Connect Your Domain</h3>
|
||||
<p className="text-blue-100 text-sm mt-1">Follow these steps to point your domain to Spark</p>
|
||||
</div>
|
||||
|
||||
{/* Steps */}
|
||||
<div className="p-6">
|
||||
{/* Step Navigator */}
|
||||
<div className="flex justify-between mb-6">
|
||||
{steps.map((step, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => setActiveStep(index)}
|
||||
className={`flex-1 py-2 text-sm font-medium transition-colors ${index === activeStep
|
||||
? 'text-blue-400 border-b-2 border-blue-400'
|
||||
: 'text-slate-500 hover:text-slate-300'
|
||||
}`}
|
||||
>
|
||||
Step {index + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Active Step Content */}
|
||||
<div className="min-h-[200px]">
|
||||
<h4 className="text-lg font-semibold text-white mb-4">
|
||||
{steps[activeStep].title}
|
||||
</h4>
|
||||
{steps[activeStep].content}
|
||||
</div>
|
||||
|
||||
{/* Navigation Buttons */}
|
||||
<div className="flex justify-between mt-6 pt-4 border-t border-slate-700">
|
||||
<button
|
||||
onClick={() => setActiveStep(Math.max(0, activeStep - 1))}
|
||||
disabled={activeStep === 0}
|
||||
className="px-4 py-2 text-slate-400 hover:text-white disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
← Previous
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveStep(Math.min(steps.length - 1, activeStep + 1))}
|
||||
disabled={activeStep === steps.length - 1}
|
||||
className="px-4 py-2 bg-slate-700 hover:bg-slate-600 text-white rounded disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Next →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { Label } from '@/components/ui/label';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Site } from '@/types/schema';
|
||||
import DomainSetupGuide from '@/components/admin/DomainSetupGuide';
|
||||
|
||||
interface SiteEditorProps {
|
||||
id: string; // Astro passes string params
|
||||
@@ -199,6 +200,9 @@ export default function SiteEditor({ id }: SiteEditorProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Domain Setup Guide */}
|
||||
<DomainSetupGuide siteDomain={site.domain} />
|
||||
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button variant="outline" onClick={() => window.history.back()}>
|
||||
Cancel
|
||||
|
||||
@@ -35,19 +35,42 @@ export default function SiteList() {
|
||||
<CardTitle className="text-sm font-medium text-slate-200">
|
||||
{site.name}
|
||||
</CardTitle>
|
||||
<Badge variant={site.status === 'active' ? 'default' : 'secondary'}>
|
||||
<Badge className={site.status === 'active' ? 'bg-green-600' : 'bg-slate-600'}>
|
||||
{site.status}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-white mb-2">{site.domain}</div>
|
||||
<p className="text-xs text-slate-500">
|
||||
{site.settings?.template || 'Default Template'}
|
||||
<div className="text-2xl font-bold text-white mb-2">{site.domain || 'No domain set'}</div>
|
||||
<p className="text-xs text-slate-500 mb-4">
|
||||
{site.domain ? '🟢 Domain configured' : '⚠️ Set up domain'}
|
||||
</p>
|
||||
<div className="mt-4 flex gap-2">
|
||||
<Button variant="outline" size="sm" className="w-full">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.location.href = `/admin/sites/${site.id}`;
|
||||
}}
|
||||
>
|
||||
Configure
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (site.domain) {
|
||||
window.open(`https://${site.domain}`, '_blank');
|
||||
} else {
|
||||
alert('Set up a domain first in site settings');
|
||||
}
|
||||
}}
|
||||
>
|
||||
👁️ Preview
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
132
frontend/src/pages/preview/page/[pageId].astro
Normal file
132
frontend/src/pages/preview/page/[pageId].astro
Normal file
@@ -0,0 +1,132 @@
|
||||
---
|
||||
/**
|
||||
* Preview Page Route
|
||||
* Shows a single page in preview mode
|
||||
*/
|
||||
|
||||
import { getDirectusClient, readItem } from '@/lib/directus/client';
|
||||
import type { Page } from '@/types/schema';
|
||||
|
||||
const { pageId } = Astro.params;
|
||||
|
||||
if (!pageId) {
|
||||
return Astro.redirect('/admin/pages');
|
||||
}
|
||||
|
||||
let page: Page | null = null;
|
||||
let error: string | null = null;
|
||||
|
||||
try {
|
||||
const client = getDirectusClient();
|
||||
const result = await client.request(readItem('pages', pageId));
|
||||
page = result as Page;
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Failed to load page';
|
||||
console.error('Preview error:', err);
|
||||
}
|
||||
|
||||
if (!page) {
|
||||
return Astro.redirect('/admin/pages');
|
||||
}
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Preview: {page.title}</title>
|
||||
<style>
|
||||
.preview-banner {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
z-index: 9999;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
margin-top: 60px;
|
||||
padding: 20px;
|
||||
max-width: 1200px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.preview-badge {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.close-preview {
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.close-preview:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Preview Mode Banner -->
|
||||
<div class="preview-banner">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
<span style="font-weight: 600; font-size: 14px;">PREVIEW MODE</span>
|
||||
<span class="preview-badge">Draft</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 12px; align-items: center;">
|
||||
<span style="font-size: 13px; opacity: 0.9;">{page.title}</span>
|
||||
<button class="close-preview" onclick="window.close()">Close Preview</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page Content -->
|
||||
<div class="preview-content">
|
||||
{error ? (
|
||||
<div style="background: #fee; border: 1px solid #fcc; color: #c00; padding: 20px; border-radius: 8px;">
|
||||
<h2>Error Loading Preview</h2>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<h1 style="font-size: 2.5rem; margin-bottom: 1rem; font-weight: 700;">
|
||||
{page.title}
|
||||
</h1>
|
||||
|
||||
{page.content && (
|
||||
<div style="line-height: 1.8; color: #333;">
|
||||
<Fragment set:html={page.content} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!page.content && (
|
||||
<p style="color: #999; font-style: italic;">No content yet</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
156
frontend/src/pages/preview/post/[postId].astro
Normal file
156
frontend/src/pages/preview/post/[postId].astro
Normal file
@@ -0,0 +1,156 @@
|
||||
---
|
||||
/**
|
||||
* Preview Post/Article Route
|
||||
* Shows a single generated article in preview mode
|
||||
*/
|
||||
|
||||
import { getDirectusClient, readItem } from '@/lib/directus/client';
|
||||
import type { GeneratedArticle } from '@/types/schema';
|
||||
|
||||
const { postId } = Astro.params;
|
||||
|
||||
if (!postId) {
|
||||
return Astro.redirect('/admin/seo/articles');
|
||||
}
|
||||
|
||||
let article: GeneratedArticle | null = null;
|
||||
let error: string | null = null;
|
||||
|
||||
try {
|
||||
const client = getDirectusClient();
|
||||
const result = await client.request(readItem('generated_articles', postId));
|
||||
article = result as GeneratedArticle;
|
||||
} catch (err) {
|
||||
error = err instanceof Error ? err.message : 'Failed to load article';
|
||||
console.error('Preview error:', err);
|
||||
}
|
||||
|
||||
if (!article) {
|
||||
return Astro.redirect('/admin/seo/articles');
|
||||
}
|
||||
---
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Preview: {article.title}</title>
|
||||
{article.meta_desc && <meta name="description" content={article.meta_desc}>}
|
||||
<style>
|
||||
.preview-banner {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 12px 20px;
|
||||
z-index: 9999;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-content {
|
||||
margin-top: 60px;
|
||||
padding: 40px 20px;
|
||||
max-width: 800px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.preview-badge {
|
||||
background: rgba(255,255,255,0.2);
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.close-preview, .share-preview {
|
||||
background: rgba(255,255,255,0.2);
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.close-preview:hover, .share-preview:hover {
|
||||
background: rgba(255,255,255,0.3);
|
||||
}
|
||||
|
||||
article {
|
||||
font-family: Georgia, serif;
|
||||
line-height: 1.8;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
article h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
article p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Preview Mode Banner -->
|
||||
<div class="preview-banner">
|
||||
<div style="display: flex; align-items: center; gap: 12px;">
|
||||
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
<span style="font-weight: 600; font-size: 14px;">PREVIEW MODE</span>
|
||||
<span class="preview-badge">{article.is_published ? 'Published' : 'Draft'}</span>
|
||||
</div>
|
||||
<div style="display: flex; gap: 12px; align-items: center;">
|
||||
<button class="share-preview" onclick="navigator.clipboard.writeText(window.location.href); alert('Preview URL copied!')">
|
||||
📋 Copy Link
|
||||
</button>
|
||||
<button class="close-preview" onclick="window.close()">Close Preview</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Article Content -->
|
||||
<div class="preview-content">
|
||||
{error ? (
|
||||
<div style="background: #fee; border: 1px solid #fcc; color: #c00; padding: 20px; border-radius: 8px;">
|
||||
<h2>Error Loading Preview</h2>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
) : (
|
||||
<article>
|
||||
<h1>{article.title}</h1>
|
||||
|
||||
{article.meta_desc && (
|
||||
<p style="color: #666; font-size: 1.1rem; font-style: italic; margin-bottom: 2rem;">
|
||||
{article.meta_desc}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{article.html_content && (
|
||||
<div>
|
||||
<Fragment set:html={article.html_content} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!article.html_content && (
|
||||
<p style="color: #999; font-style: italic;">No content generated yet</p>
|
||||
)}
|
||||
</article>
|
||||
)}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user