Files
firefrost-operations-manual/tools/ghost-page-builder/ghost-page-builder.jsx
Claude 7e49af8494 fix: Use nicknames in Ghost Page Builder templates (not real names)
PRIVACY FIX:
- Trinity Layout template: Real names → Nicknames
  - Meg → GingerFury
  - Holly → unicorn20089
  - Michael → Frostystyle
- Trinity Card Grid template: Same fix

POLICY:
Public-facing content uses nicknames only.
Real names only in internal documentation.

Caught by Michael during testing checkpoint.

Signed-off-by: Chronicler #39 <claude@firefrostgaming.com>
2026-03-21 21:51:16 +00:00

451 lines
14 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
// Fire/Frost/Arcane CSS definitions (injected into preview)
const FIRE_FROST_CSS = `
:root {
/* Fire Colors (Meg/The Emissary) */
--fire-primary: #FF6B35;
--fire-secondary: #F7931E;
--fire-accent: #FFA500;
/* Frost Colors (Michael/The Wizard) */
--frost-primary: #4ECDC4;
--frost-secondary: #45B7D1;
--frost-accent: #00CED1;
/* Arcane Colors (Holly/The Catalyst) */
--arcane-primary: #A855F7;
--arcane-secondary: #9D4EDD;
--arcane-accent: #C77DFF;
--arcane-dark: #7F00FF;
/* Neutral Colors */
--neutral-dark: #1a1a1a;
--neutral-gray: #6b7280;
--neutral-light: #f3f4f6;
}
/* Fire/Frost/Arcane Gradient Utilities */
.fire-frost-gradient {
background: linear-gradient(135deg, #FF6B35 0%, #4ECDC4 100%);
}
.fire-gradient {
background: linear-gradient(135deg, #FF6B35 0%, #FFA500 100%);
}
.frost-gradient {
background: linear-gradient(135deg, #4ECDC4 0%, #00CED1 100%);
}
.arcane-gradient {
background: linear-gradient(135deg, #A855F7 0%, #C77DFF 100%);
}
.trinity-gradient {
background: linear-gradient(135deg, #FF6B35 0%, #A855F7 50%, #4ECDC4 100%);
}
.arcane-storm-gradient {
background: linear-gradient(135deg, #7F00FF 0%, #A855F7 50%, #C77DFF 100%);
}
/* Typography Colors */
.fire-text {
color: var(--fire-primary);
}
.frost-text {
color: var(--frost-primary);
}
.arcane-text {
color: var(--arcane-primary);
}
/* Border Utilities */
.fire-border {
border-color: var(--fire-primary);
}
.frost-border {
border-color: var(--frost-primary);
}
.arcane-border {
border-color: var(--arcane-primary);
}
/* Common Ghost page styles */
.gh-content {
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}
.gh-content h1 {
font-size: 3rem;
font-weight: 700;
margin-bottom: 1.5rem;
line-height: 1.2;
}
.gh-content h2 {
font-size: 2rem;
font-weight: 600;
margin: 2rem 0 1rem;
line-height: 1.3;
}
.gh-content p {
font-size: 1.125rem;
line-height: 1.75;
margin-bottom: 1.5rem;
}
.gh-content a {
color: var(--fire-primary);
text-decoration: underline;
}
.gh-content a:hover {
color: var(--frost-primary);
}
`;
// Sample templates with Ghost wrapper classes
const SAMPLE_TEMPLATES = {
blank: {
name: 'Blank Page',
html: '<h1>Page Title</h1>\n<p>Start writing your content here...</p>'
},
simple: {
name: 'Simple Page',
html: `<h1>Welcome to Firefrost Gaming</h1>
<p>This is a simple page template with basic structure.</p>
<h2>Section Heading</h2>
<p>Add your content here. This template includes proper Ghost content classes for consistent styling.</p>
<p>You can add more paragraphs, headings, and content as needed.</p>`
},
twoColumn: {
name: 'Trinity Layout (3 Columns)',
html: `<h1>The Trinity</h1>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 2rem; margin: 2rem 0;">
<div>
<h2 class="fire-text">🔥 Fire Path</h2>
<p><strong>GingerFury - The Emissary</strong></p>
<p>Community-focused content goes here. Passionate, warm, welcoming.</p>
<ul>
<li>Community events</li>
<li>Player stories</li>
<li>Social activities</li>
</ul>
</div>
<div>
<h2 class="arcane-text">⚡ Arcane Path</h2>
<p><strong>unicorn20089 - The Catalyst</strong></p>
<p>Creative, building-focused content. Innovation, transformation, foundation.</p>
<ul>
<li>Build showcases</li>
<li>Creative projects</li>
<li>World design</li>
</ul>
</div>
<div>
<h2 class="frost-text">❄️ Frost Path</h2>
<p><strong>Frostystyle - The Wizard</strong></p>
<p>Technical, precise content goes here. Cool, calculated, systematic.</p>
<ul>
<li>Server specifications</li>
<li>Technical guides</li>
<li>Performance metrics</li>
</ul>
</div>
</div>`
},
cardGrid: {
name: 'Trinity Card Grid',
html: `<h1>The Trinity - Fire + Arcane + Frost</h1>
<p>Showcase the three elemental forces.</p>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2rem; margin: 2rem 0;">
<div style="border: 2px solid var(--fire-primary); border-radius: 8px; padding: 1.5rem;">
<h3 class="fire-text">🔥 Fire - The Emissary</h3>
<p>Passion, community, warmth. GingerFury brings the heart and the people.</p>
</div>
<div style="border: 2px solid var(--arcane-primary); border-radius: 8px; padding: 1.5rem;">
<h3 class="arcane-text">⚡ Arcane - The Catalyst</h3>
<p>Creative transformation, building, innovation. unicorn20089 brings the foundation.</p>
</div>
<div style="border: 2px solid var(--frost-primary); border-radius: 8px; padding: 1.5rem;">
<h3 class="frost-text">❄️ Frost - The Wizard</h3>
<p>Precision, technical excellence, strategy. Frostystyle brings the architecture.</p>
</div>
</div>`
}
};
// Helper function to generate preview HTML for iframe srcdoc
const generatePreviewHtml = (htmlContent) => {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ghost Preview</title>
<!-- Ghost Theme CSS (Source v1.5.2) -->
<link rel="stylesheet" href="https://firefrostgaming.com/assets/built/screen.css">
<!-- Fire/Frost Custom CSS -->
<style>${FIRE_FROST_CSS}</style>
</head>
<body class="gh-body" style="margin: 0; padding: 0; background: white;">
<div class="gh-content">
${htmlContent}
</div>
</body>
</html>`;
};
export default function GhostPageBuilder() {
// Two-state pattern: instant input + debounced preview
const [htmlInput, setHtmlInput] = useState('');
const [debouncedHtml, setDebouncedHtml] = useState('');
const [viewport, setViewport] = useState('desktop');
const [copied, setCopied] = useState(false);
// Load saved draft from localStorage on mount
useEffect(() => {
const saved = localStorage.getItem('firefrost_ghost_draft');
if (saved) {
setHtmlInput(saved);
setDebouncedHtml(saved);
} else {
// Start with simple template if no saved draft
setHtmlInput(SAMPLE_TEMPLATES.simple.html);
setDebouncedHtml(SAMPLE_TEMPLATES.simple.html);
}
}, []);
// Debounce preview updates and auto-save to localStorage
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedHtml(htmlInput);
localStorage.setItem('firefrost_ghost_draft', htmlInput);
}, 500);
return () => clearTimeout(timer);
}, [htmlInput]);
// Tab key handler for textarea
const handleKeyDown = (e) => {
if (e.key === 'Tab') {
e.preventDefault();
const start = e.target.selectionStart;
const end = e.target.selectionEnd;
// Insert two spaces at cursor position
const newValue = htmlInput.substring(0, start) + " " + htmlInput.substring(end);
setHtmlInput(newValue);
// Move cursor after inserted spaces (wait for React state update)
setTimeout(() => {
e.target.selectionStart = e.target.selectionEnd = start + 2;
}, 0);
}
};
// Copy HTML to clipboard
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(htmlInput);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error('Failed to copy:', err);
}
};
// Load sample template
const loadTemplate = (templateKey) => {
setHtmlInput(SAMPLE_TEMPLATES[templateKey].html);
};
// Clear editor
const handleReset = () => {
if (confirm('Clear the editor? Your current work will be lost.')) {
setHtmlInput('');
localStorage.removeItem('firefrost_ghost_draft');
}
};
// Viewport dimensions
const viewportWidths = {
desktop: '100%',
tablet: '768px',
mobile: '375px'
};
return (
<div className="flex flex-col h-screen bg-gray-50">
{/* Header */}
<div className="bg-gradient-to-r from-orange-500 via-purple-500 to-teal-400 text-white p-4 shadow-lg">
<h1 className="text-2xl font-bold">🔥 Ghost Page Builder</h1>
<p className="text-sm opacity-90">Live preview with Fire + Arcane + Frost CSS</p>
</div>
{/* Toolbar */}
<div className="bg-white border-b border-gray-200 p-3 flex items-center gap-4 flex-wrap">
{/* Sample Templates Dropdown */}
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-gray-700">Template:</label>
<select
onChange={(e) => loadTemplate(e.target.value)}
className="px-3 py-1.5 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-orange-500"
>
<option value="">-- Select Template --</option>
{Object.entries(SAMPLE_TEMPLATES).map(([key, template]) => (
<option key={key} value={key}>{template.name}</option>
))}
</select>
</div>
{/* Viewport Toggle */}
<div className="flex items-center gap-2">
<label className="text-sm font-medium text-gray-700">Viewport:</label>
<div className="flex gap-1">
<button
onClick={() => setViewport('desktop')}
className={`px-3 py-1.5 text-sm rounded ${
viewport === 'desktop'
? 'bg-orange-500 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Desktop
</button>
<button
onClick={() => setViewport('tablet')}
className={`px-3 py-1.5 text-sm rounded ${
viewport === 'tablet'
? 'bg-orange-500 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Tablet
</button>
<button
onClick={() => setViewport('mobile')}
className={`px-3 py-1.5 text-sm rounded ${
viewport === 'mobile'
? 'bg-orange-500 text-white'
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
}`}
>
Mobile
</button>
</div>
</div>
{/* Spacer */}
<div className="flex-1"></div>
{/* Action Buttons */}
<button
onClick={handleCopy}
className="px-4 py-1.5 bg-teal-500 text-white rounded hover:bg-teal-600 text-sm font-medium transition-colors"
>
{copied ? '✓ Copied!' : 'Copy HTML'}
</button>
<button
onClick={handleReset}
className="px-4 py-1.5 bg-gray-200 text-gray-700 rounded hover:bg-gray-300 text-sm font-medium transition-colors"
>
Reset
</button>
</div>
{/* Main Content: Split Pane */}
<div className="flex-1 flex overflow-hidden">
{/* Left: Editor */}
<div className="w-1/2 flex flex-col border-r border-gray-200">
<div className="bg-gray-800 text-white px-4 py-2 text-sm font-medium">
HTML Editor
</div>
<textarea
value={htmlInput}
onChange={(e) => setHtmlInput(e.target.value)}
onKeyDown={handleKeyDown}
className="flex-1 p-4 font-mono text-sm bg-gray-900 text-gray-100 resize-none outline-none"
spellCheck="false"
placeholder="Paste or write your HTML here..."
/>
</div>
{/* Right: Preview */}
<div className="w-1/2 flex flex-col bg-gray-100">
<div className="bg-gray-800 text-white px-4 py-2 text-sm font-medium flex items-center justify-between">
<span>Live Preview</span>
<span className="text-xs opacity-75">
{viewport === 'desktop' ? 'Full Width' : viewportWidths[viewport]}
</span>
</div>
<div className="flex-1 overflow-auto p-4 flex justify-center">
{/* Viewport Wrapper */}
<div
className="transition-all duration-300 ease-in-out shadow-lg"
style={{
width: viewportWidths[viewport],
height: '100%',
maxHeight: '100%'
}}
>
<iframe
title="Ghost Preview"
srcDoc={generatePreviewHtml(debouncedHtml)}
className="w-full h-full border-none bg-white rounded"
sandbox="allow-same-origin allow-scripts"
/>
</div>
</div>
</div>
</div>
{/* Footer with Color Reference */}
<div className="bg-white border-t border-gray-200 p-3">
<div className="flex items-center gap-4 text-xs text-gray-600 flex-wrap">
<span className="font-medium">Trinity Colors:</span>
<div className="flex items-center gap-1">
<span className="w-4 h-4 rounded" style={{background: '#FF6B35'}}></span>
<span>Fire #FF6B35</span>
</div>
<div className="flex items-center gap-1">
<span className="w-4 h-4 rounded" style={{background: '#A855F7'}}></span>
<span>Arcane #A855F7</span>
</div>
<div className="flex items-center gap-1">
<span className="w-4 h-4 rounded" style={{background: '#4ECDC4'}}></span>
<span>Frost #4ECDC4</span>
</div>
<div className="flex items-center gap-1">
<span className="w-4 h-4 rounded" style={{background: 'linear-gradient(135deg, #FF6B35 0%, #A855F7 50%, #4ECDC4 100%)'}}></span>
<span>Trinity Gradient</span>
</div>
</div>
</div>
</div>
);
}