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:
cawcenter
2025-12-13 10:42:37 -05:00
parent aa46b98ce7
commit 3e5eba4a1f
5 changed files with 488 additions and 5 deletions

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

View File

@@ -7,6 +7,7 @@ import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Site } from '@/types/schema'; import { Site } from '@/types/schema';
import DomainSetupGuide from '@/components/admin/DomainSetupGuide';
interface SiteEditorProps { interface SiteEditorProps {
id: string; // Astro passes string params id: string; // Astro passes string params
@@ -199,6 +200,9 @@ export default function SiteEditor({ id }: SiteEditorProps) {
</CardContent> </CardContent>
</Card> </Card>
{/* Domain Setup Guide */}
<DomainSetupGuide siteDomain={site.domain} />
<div className="flex justify-end gap-4"> <div className="flex justify-end gap-4">
<Button variant="outline" onClick={() => window.history.back()}> <Button variant="outline" onClick={() => window.history.back()}>
Cancel Cancel

View File

@@ -35,19 +35,42 @@ export default function SiteList() {
<CardTitle className="text-sm font-medium text-slate-200"> <CardTitle className="text-sm font-medium text-slate-200">
{site.name} {site.name}
</CardTitle> </CardTitle>
<Badge variant={site.status === 'active' ? 'default' : 'secondary'}> <Badge className={site.status === 'active' ? 'bg-green-600' : 'bg-slate-600'}>
{site.status} {site.status}
</Badge> </Badge>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-2xl font-bold text-white mb-2">{site.domain}</div> <div className="text-2xl font-bold text-white mb-2">{site.domain || 'No domain set'}</div>
<p className="text-xs text-slate-500"> <p className="text-xs text-slate-500 mb-4">
{site.settings?.template || 'Default Template'} {site.domain ? '🟢 Domain configured' : '⚠️ Set up domain'}
</p> </p>
<div className="mt-4 flex gap-2"> <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 Configure
</Button> </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> </div>
</CardContent> </CardContent>
</Card> </Card>

View 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>

View 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>