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>
451 lines
14 KiB
JavaScript
451 lines
14 KiB
JavaScript
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>
|
||
);
|
||
}
|