- Remove iframe previews from theme detail and layout detail pages for cleaner UI - Replace with informative layout cards showing descriptions and slot type badges - Fix theme hot reload by switching from custom HMR to full page reload - Update template renderer to use slot names as demo content - Add SVG pattern generation for image slots with grid background and slot labels - Improve demo content for all slot types (code, lists, tables, etc.) - Clean up HMR listeners and simplify theme change detection 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
193 lines
6.4 KiB
TypeScript
193 lines
6.4 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
||
import { useParams, Link } from 'react-router-dom';
|
||
import type { Theme, SlideLayout } from '../../types/theme';
|
||
import { getTheme } from '../../themes';
|
||
import './LayoutDetailPage.css';
|
||
|
||
export const LayoutDetailPage: React.FC = () => {
|
||
const { themeId, layoutId } = useParams<{ themeId: string; layoutId: string }>();
|
||
const [theme, setTheme] = useState<Theme | null>(null);
|
||
const [layout, setLayout] = useState<SlideLayout | null>(null);
|
||
const [loading, setLoading] = useState(true);
|
||
const [error, setError] = useState<string | null>(null);
|
||
const [viewMode, setViewMode] = useState<'template' | 'slots'>('slots');
|
||
|
||
useEffect(() => {
|
||
const loadThemeAndLayout = async () => {
|
||
if (!themeId || !layoutId) {
|
||
setError('Missing theme ID or layout ID');
|
||
setLoading(false);
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setLoading(true);
|
||
const themeData = await getTheme(themeId);
|
||
if (!themeData) {
|
||
setError(`Theme "${themeId}" not found`);
|
||
return;
|
||
}
|
||
|
||
const layoutData = themeData.layouts.find((l: SlideLayout) => l.id === layoutId);
|
||
if (!layoutData) {
|
||
setError(`Layout "${layoutId}" not found in theme "${themeId}"`);
|
||
return;
|
||
}
|
||
|
||
setTheme(themeData);
|
||
setLayout(layoutData);
|
||
} catch (err) {
|
||
setError(err instanceof Error ? err.message : 'Failed to load theme and layout');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
loadThemeAndLayout();
|
||
}, [themeId, layoutId]);
|
||
|
||
if (loading) {
|
||
return (
|
||
<div className="layout-detail-page">
|
||
<div className="loading-spinner">Loading layout...</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error) {
|
||
return (
|
||
<div className="layout-detail-page">
|
||
<div className="error-message">
|
||
<h2>Error</h2>
|
||
<p>{error}</p>
|
||
<Link to="/themes" className="back-link">← Back to Themes</Link>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (!theme || !layout) {
|
||
return (
|
||
<div className="layout-detail-page">
|
||
<div className="not-found">
|
||
<h2>Layout Not Found</h2>
|
||
<p>The requested layout could not be found.</p>
|
||
<Link to="/themes" className="back-link">← Back to Themes</Link>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div className="layout-detail-page">
|
||
<header className="layout-detail-header">
|
||
<nav className="breadcrumb">
|
||
<Link to="/themes">Themes</Link>
|
||
<span className="separator">›</span>
|
||
<Link to={`/themes/${theme.id}`}>{theme.name}</Link>
|
||
<span className="separator">›</span>
|
||
<span className="current">{layout.name}</span>
|
||
</nav>
|
||
|
||
<div className="layout-info">
|
||
<h1 className="layout-name">{layout.name}</h1>
|
||
<p className="layout-description">{layout.description}</p>
|
||
<div className="layout-stats">
|
||
<span className="slot-count">{layout.slots.length} slots</span>
|
||
<span className="theme-name">from {theme.name}</span>
|
||
<Link
|
||
to={`/themes/${theme.id}/layouts/${layout.id}/preview`}
|
||
className="preview-link"
|
||
>
|
||
View Full Preview →
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<div className="view-mode-selector">
|
||
<button
|
||
type="button"
|
||
className={`mode-button ${viewMode === 'slots' ? 'active' : ''}`}
|
||
onClick={() => setViewMode('slots')}
|
||
>
|
||
Slots
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={`mode-button ${viewMode === 'template' ? 'active' : ''}`}
|
||
onClick={() => setViewMode('template')}
|
||
>
|
||
Template
|
||
</button>
|
||
</div>
|
||
|
||
<main className="layout-content">
|
||
{viewMode === 'template' && (
|
||
<section className="template-section">
|
||
<h2>HTML Template</h2>
|
||
<div className="template-code">
|
||
<pre><code>{layout.htmlTemplate}</code></pre>
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{viewMode === 'slots' && (
|
||
<section className="slots-section">
|
||
<h2>Slot Configuration</h2>
|
||
<div className="slots-grid">
|
||
{layout.slots.map((slot) => (
|
||
<div key={slot.id} className="slot-card">
|
||
<div className="slot-header">
|
||
<h3 className="slot-id">{slot.id}</h3>
|
||
<span className={`slot-type ${slot.type}`}>{slot.type}</span>
|
||
{slot.required && <span className="required-badge">Required</span>}
|
||
</div>
|
||
|
||
<div className="slot-details">
|
||
{slot.placeholder && (
|
||
<div className="slot-field">
|
||
<strong>Placeholder:</strong> {slot.placeholder}
|
||
</div>
|
||
)}
|
||
|
||
{slot.defaultContent && (
|
||
<div className="slot-field">
|
||
<strong>Default Content:</strong> {slot.defaultContent}
|
||
</div>
|
||
)}
|
||
|
||
{slot.className && (
|
||
<div className="slot-field">
|
||
<strong>CSS Class:</strong> <code>{slot.className}</code>
|
||
</div>
|
||
)}
|
||
|
||
{slot.style && (
|
||
<div className="slot-field">
|
||
<strong>Inline Style:</strong> <code>{slot.style}</code>
|
||
</div>
|
||
)}
|
||
|
||
{slot.attributes && Object.keys(slot.attributes).length > 0 && (
|
||
<div className="slot-field">
|
||
<strong>Attributes:</strong>
|
||
<div className="attributes-list">
|
||
{Object.entries(slot.attributes).map(([key, value]) => (
|
||
<div key={key} className="attribute-item">
|
||
<code>{key}="{value}"</code>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
)}
|
||
</main>
|
||
</div>
|
||
);
|
||
}; |