blueprints/gallery.html.template
2025-11-13 20:34:22 +01:00

1036 lines
33 KiB
Text

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WordPress Blueprints Gallery</title>
<script type="module">
import { h, render, Component } from 'https://esm.sh/preact@10.19.3';
import { useState, useEffect } from 'https://esm.sh/preact@10.19.3/hooks';
window.h = h;
window.Component = Component;
window.useState = useState;
window.useEffect = useEffect;
window.render = render;
</script>
<script id="blueprint-data" type="application/json">{BLUEPRINT_INDEX_JSON}</script>
<style>
:root {
color-scheme: light;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
overflow-y: scroll;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
line-height: 1.6;
color: #1f2937;
background: #f3f4f6;
}
a {
color: inherit;
}
.page {
min-height: 100vh;
display: flex;
flex-direction: column;
background: #f3f4f6;
}
.hero {
background: #1f2937;
color: #f9fafb;
padding: 3rem 0 2.75rem;
box-shadow: 0 12px 40px rgba(15, 23, 42, 0.35);
}
.hero-inner {
max-width: 1200px;
margin: 0 auto;
padding: 0 2rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.hero-header {
display: flex;
align-items: center;
gap: 1rem;
}
.hero-logo {
width: 48px;
height: 48px;
}
.hero-title {
font-size: 2.25rem;
font-weight: 700;
margin: 0;
}
.hero-subtitle {
font-size: 1.05rem;
color: #d1d5db;
max-width: 820px;
line-height: 1.7;
}
.hero-link {
color: #60a5fa;
text-decoration: none;
font-weight: 600;
}
.hero-link:hover {
text-decoration: underline;
}
.filters-section {
background: #f9fafb;
border-bottom: 1px solid #e5e7eb;
}
.filters-inner {
max-width: 1200px;
margin: 0 auto;
padding: 1.75rem 2rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.tab-list {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.tab {
padding: 0.6rem 1.2rem;
border-radius: 999px;
border: 1px solid #d1d5db;
background: #ffffff;
color: #1f2937;
font-weight: 600;
font-size: 0.95rem;
cursor: pointer;
transition: all 0.2s ease;
}
.tab:hover {
border-color: #94a3b8;
}
.tab.active {
background: #0f172a;
border-color: #0f172a;
color: #f9fafb;
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.25);
}
.toolbar {
display: flex;
gap: 1rem;
align-items: center;
}
.search-block {
flex: 1;
max-width: 500px;
}
.search-field {
position: relative;
}
.search-field input {
width: 100%;
padding: 0.7rem 1rem 0.7rem 2.6rem;
border-radius: 12px;
border: 1px solid #d1d5db;
background: #ffffff;
font-size: 0.95rem;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.search-field input:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}
.search-icon {
position: absolute;
top: 50%;
left: 0.9rem;
transform: translateY(-50%);
color: #9ca3af;
font-size: 1.35rem;
pointer-events: none;
}
.gallery-section {
flex: 1;
}
.gallery-inner {
max-width: 1200px;
margin: 0 auto;
padding: 2rem 2rem 3rem;
}
.results-summary {
margin-bottom: 1.5rem;
color: #6b7280;
font-size: 0.95rem;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.75rem;
}
.skeleton-grid {
opacity: 0.9;
}
.pattern-card {
background: #ffffff;
border-radius: 4px;
overflow: hidden;
border: 1px solid #e5e7eb;
box-shadow: 0 15px 35px rgba(15, 23, 42, 0.08);
transition: transform 0.2s ease, box-shadow 0.2s ease;
display: flex;
flex-direction: column;
}
.pattern-card:hover {
transform: translateY(-4px);
box-shadow: 0 22px 45px rgba(15, 23, 42, 0.15);
}
.pattern-card-image {
display: block;
width: 100%;
height: 230px;
object-fit: cover;
object-position: center top;
background: #f3f4f6;
}
.pattern-card-image.placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 230px;
font-size: 0.95rem;
color: #9ca3af;
}
.pattern-card-body {
padding: 1.25rem 1.5rem 1.4rem;
display: flex;
flex-direction: column;
gap: 0.9rem;
flex: 1;
}
.pattern-card-header {
display: flex;
align-items: flex-start;
gap: 0.75rem;
flex-wrap: nowrap;
}
.pattern-card-title {
margin: 0;
font-size: 1.05rem;
font-weight: 600;
color: #111827;
flex: 1 1 auto;
min-width: 0;
}
.pattern-card-description {
margin: 0;
font-size: 0.9rem;
color: #6b7280;
min-height: 2.4rem;
}
.pattern-card-meta {
margin: 0;
font-size: 0.85rem;
color: #6b7280;
margin-top: auto;
}
.pattern-card-meta a {
color: #2563eb;
text-decoration: none;
font-weight: 600;
}
.pattern-card-meta a:hover {
text-decoration: underline;
}
.pattern-card-meta .meta-separator {
color: #d1d5db;
margin: 0 0.25rem;
font-weight: 600;
}
.btn-try-it {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.65rem 1.25rem;
background: #3858e9;
color: #ffffff;
border-radius: 8px;
font-weight: 600;
font-size: 0.95rem;
text-decoration: none;
transition: background 0.2s ease, transform 0.1s ease;
flex-shrink: 0;
}
.pattern-card-header .btn-try-it {
margin-left: auto;
}
.btn-try-it:hover {
background: #2a44d0;
transform: translateY(-1px);
}
.btn-try-it .btn-icon {
width: 18px;
height: 18px;
}
.btn-view-source {
color: #6b7280;
font-weight: 600;
font-size: 0.9rem;
text-decoration: none;
}
.btn-view-source:hover {
color: #2563eb;
text-decoration: underline;
}
.empty-state {
margin-top: 3rem;
text-align: center;
padding: 3rem;
background: #ffffff;
border-radius: 16px;
border: 1px solid #e5e7eb;
color: #6b7280;
box-shadow: 0 15px 35px rgba(15, 23, 42, 0.08);
}
.empty-state h2 {
font-size: 1.6rem;
color: #111827;
margin-bottom: 0.75rem;
}
.placeholder-card {
background: #ffffff;
border-radius: 4px;
border: 1px solid #e5e7eb;
padding-bottom: 1.25rem;
box-shadow: 0 10px 25px rgba(15, 23, 42, 0.08);
display: flex;
flex-direction: column;
gap: 1rem;
}
.placeholder-thumb {
height: 230px;
background: #e5e7eb;
}
.placeholder-body {
padding: 0 1.5rem;
display: flex;
flex-direction: column;
gap: 0.7rem;
}
.placeholder-line {
height: 12px;
border-radius: 999px;
background: #e5e7eb;
}
.placeholder-line.wide {
width: 70%;
}
.placeholder-line.medium {
width: 55%;
}
.placeholder-line.short {
width: 40%;
}
.skeleton-animate {
position: relative;
overflow: hidden;
}
.skeleton-animate::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, rgba(255,255,255,0) 0%, rgba(255,255,255,0.5) 50%, rgba(255,255,255,0) 100%);
transform: translateX(-100%);
animation: shimmer 1.4s infinite;
}
@keyframes shimmer {
100% {
transform: translateX(100%);
}
}
.no-results h2 {
font-size: 1.6rem;
color: #111827;
margin-bottom: 0.75rem;
}
.footer {
background: #f9fafb;
padding: 3rem 2rem 3.5rem;
color: #6b7280;
font-size: 0.9rem;
text-align: center;
border-top: 1px solid #e5e7eb;
}
.footer a {
color: #2563eb;
text-decoration: none;
font-weight: 600;
}
.footer a:hover {
text-decoration: underline;
}
@media (max-width: 960px) {
.global-nav-inner {
gap: 1rem;
}
.hero-title {
font-size: 2.4rem;
}
.toolbar {
flex-direction: column;
align-items: flex-start;
}
.dropdown-group {
width: 100%;
justify-content: flex-start;
}
.search-block {
width: 100%;
}
}
@media (max-width: 600px) {
.global-nav-inner {
padding: 0.65rem 1.25rem;
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
}
.hero-inner {
padding: 0 1.25rem;
}
.filters-inner {
padding: 1.5rem 1.25rem;
}
.gallery-inner {
padding: 1.5rem 1.25rem 2.5rem;
}
.tab {
padding: 0.5rem 1rem;
font-size: 0.9rem;
}
.pattern-card-body {
padding: 1rem 1.1rem 1.1rem;
}
.pattern-card-header {
align-items: center;
}
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module">
const { h, render, useState, useEffect } = window;
const dataElement = document.getElementById('blueprint-data');
let embeddedIndex = null;
if (dataElement) {
try {
embeddedIndex = JSON.parse(dataElement.textContent || '{}');
} catch (error) {
console.error('Failed to parse embedded blueprint data.', error);
}
}
window.__EMBEDDED_BLUEPRINT_INDEX__ = embeddedIndex;
const highlightedBlueprints = [
'Stylish Press',
'Feed Reader with the Friends Plugin',
'Gaming News',
'Skincare Blog',
'Non-profit Organization',
'Personal Resume',
'Personal Blog',
'University Website',
'Photography Portfolio',
'Art Gallery'
];
function deriveBlueprintData(indexData) {
if (!indexData) {
return null;
}
const categoryCount = {};
const authors = new Set();
let highlightedTotal = 0;
const entries = Object.entries(indexData).map(([path, meta], index) => {
const categories = meta.categories || [];
categories.forEach(cat => {
categoryCount[cat] = (categoryCount[cat] || 0) + 1;
});
if (meta.author) {
authors.add(meta.author);
}
const isHighlighted = highlightedBlueprints.includes(meta.title);
if (isHighlighted) {
highlightedTotal += 1;
}
return {
path,
title: meta.title || 'Untitled Blueprint',
description: meta.description || '',
author: meta.author || '',
categories,
screenshot_url: meta.screenshot_url || '',
highlighted: isHighlighted,
order: index
};
});
const sortedCategories = Object.entries(categoryCount)
.sort((a, b) => b[1] - a[1])
.map(([name]) => name);
return {
entries,
topCategories: sortedCategories.slice(0, 8),
allCategories: sortedCategories,
stats: {
totalBlueprints: entries.length,
uniqueCategories: Object.keys(categoryCount).length,
uniqueAuthors: authors.size,
highlighted: highlightedTotal
}
};
}
const initialBlueprintData = deriveBlueprintData(embeddedIndex);
function buildPreviewUrl(path) {
return `https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/wordpress/blueprints/trunk/${path}`;
}
function buildEditUrl(path) {
return `https://playground.wordpress.net/builder/builder.html?blueprint-url=https://raw.githubusercontent.com/wordpress/blueprints/trunk/${path}`;
}
function buildSourceUrl(path) {
return `https://github.com/wordpress/blueprints/blob/trunk/${path}`;
}
function computeFavoriteCount(blueprint) {
const seed = `${blueprint.title}|${blueprint.path}`;
const hash = Array.from(seed).reduce((total, char) => total + char.charCodeAt(0), 0);
return 40 + (hash % 160);
}
function formatNumber(value) {
return value.toLocaleString('en-US');
}
function normalizeScreenshotSrc(blueprint) {
const rawPrefix = 'https://raw.githubusercontent.com/wordpress/blueprints/';
const { screenshot_url: originalSrc = '', path = '' } = blueprint;
if (originalSrc.startsWith(rawPrefix)) {
return originalSrc.replace(/^https:\/\/raw\.githubusercontent\.com\/wordpress\/blueprints\/[^/]+\//, '');
}
if (originalSrc) {
return originalSrc;
}
if (path) {
return path.replace(/blueprint\.json$/, 'screenshot.jpg');
}
return '';
}
function BlueprintCard({ blueprint }) {
const previewUrl = buildPreviewUrl(blueprint.path);
const sourceUrl = buildSourceUrl(blueprint.path);
const editUrl = buildEditUrl(blueprint.path);
const screenshotSrc = normalizeScreenshotSrc(blueprint);
const metaChildren = [];
if (blueprint.author) {
metaChildren.push('By ');
metaChildren.push(h('a', {
href: `https://github.com/${encodeURIComponent(blueprint.author)}`,
target: '_blank',
rel: 'noreferrer noopener'
}, `@${blueprint.author}`));
}
const sourceLink = h('a', {
href: sourceUrl,
target: '_blank',
rel: 'noreferrer noopener'
}, 'View source');
const editLink = h('a', {
href: editUrl,
target: '_blank',
rel: 'noreferrer noopener'
}, 'Edit');
const addSeparatorIfNeeded = () => {
if (metaChildren.length > 0) {
metaChildren.push(' ');
metaChildren.push(h('span', { className: 'meta-separator' }, '•'));
metaChildren.push(' ');
}
};
addSeparatorIfNeeded();
metaChildren.push(sourceLink);
addSeparatorIfNeeded();
metaChildren.push(editLink);
const metaLine = h('p', { className: 'pattern-card-meta' }, metaChildren);
return h('article', { className: 'pattern-card' },
screenshotSrc
? h('img', {
className: 'pattern-card-image',
src: screenshotSrc,
alt: `${blueprint.title} screenshot`,
onError: (event) => {
event.target.parentElement.innerHTML = '<div class="pattern-card-image placeholder">No screenshot available</div>';
}
})
: h('div', { className: 'pattern-card-image placeholder' }, 'No screenshot available'),
h('div', { className: 'pattern-card-body' },
h('div', { className: 'pattern-card-header' },
h('h2', { className: 'pattern-card-title' }, blueprint.title),
h('a', {
href: previewUrl,
className: 'btn-try-it',
target: '_blank',
rel: 'noreferrer noopener'
},
h('img', {
src: 'playground-icon.png',
className: 'btn-icon',
alt: ''
}),
'Run'
)
),
blueprint.description && h('p', { className: 'pattern-card-description' }, blueprint.description),
metaLine
)
);
}
function PlaceholderCard() {
return h('article', { className: 'placeholder-card' },
h('div', { className: 'placeholder-thumb skeleton-animate' }),
h('div', { className: 'placeholder-body' },
h('div', { className: 'placeholder-line wide skeleton-animate' }),
h('div', { className: 'placeholder-line medium skeleton-animate' }),
h('div', { className: 'placeholder-line short skeleton-animate' })
)
);
}
function App() {
const blueprintEntries = initialBlueprintData ? initialBlueprintData.entries : [];
const blueprints = blueprintEntries;
const topCategories = initialBlueprintData ? initialBlueprintData.topCategories : [];
const allCategories = initialBlueprintData ? initialBlueprintData.allCategories : [];
const stats = initialBlueprintData ? initialBlueprintData.stats : {
totalBlueprints: 0,
uniqueCategories: 0,
uniqueAuthors: 0,
highlighted: 0
};
const [filteredBlueprints, setFilteredBlueprints] = useState(blueprintEntries);
const [searchTerm, setSearchTerm] = useState('');
const [activeCategories, setActiveCategories] = useState(new Set());
const [showFeaturedOnly, setShowFeaturedOnly] = useState(false);
const [sortMode, setSortMode] = useState('gallery');
const [selectedFilter, setSelectedFilter] = useState('all');
const isLoaded = Boolean(initialBlueprintData);
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const searchParam = params.get('search');
const categoriesParam = params.get('categories');
const featuredParam = params.get('featured');
const sortParam = params.get('sort');
const filterParam = params.get('filter');
if (searchParam) {
setSearchTerm(searchParam);
}
if (categoriesParam) {
const categoryList = categoriesParam.split(',').filter(Boolean);
if (categoryList.length > 0) {
setActiveCategories(new Set(categoryList));
setSelectedFilter(categoryList[0]);
}
}
if (featuredParam === 'true') {
setShowFeaturedOnly(true);
setSelectedFilter('featured');
}
if (sortParam) {
setSortMode(sortParam);
}
if (filterParam) {
setSelectedFilter(filterParam === 'curated' ? 'all' : filterParam);
}
}, []);
useEffect(() => {
if (!initialBlueprintData) {
console.error('Blueprint index data missing. Rebuild gallery.html to embed it.');
}
}, []);
useEffect(() => {
const params = new URLSearchParams();
if (searchTerm) {
params.set('search', searchTerm);
}
if (activeCategories.size > 0) {
params.set('categories', Array.from(activeCategories).join(','));
}
if (showFeaturedOnly) {
params.set('featured', 'true');
}
if (sortMode && sortMode !== 'gallery') {
params.set('sort', sortMode);
}
if (selectedFilter && selectedFilter !== 'all') {
params.set('filter', selectedFilter);
}
const newUrl = params.toString()
? `${window.location.pathname}?${params.toString()}`
: window.location.pathname;
window.history.replaceState({}, '', newUrl);
}, [searchTerm, activeCategories, showFeaturedOnly, sortMode, selectedFilter]);
useEffect(() => {
const term = searchTerm.trim().toLowerCase();
const filtered = blueprints.filter(bp => {
const matchesSearch = !term ||
bp.title.toLowerCase().includes(term) ||
bp.description.toLowerCase().includes(term) ||
bp.author.toLowerCase().includes(term);
const matchesCategory = activeCategories.size === 0 ||
bp.categories.some(cat => activeCategories.has(cat));
const matchesFeatured = !showFeaturedOnly || bp.highlighted;
return matchesSearch && matchesCategory && matchesFeatured;
});
const sorted = [...filtered];
if (sortMode === 'alphabetical') {
sorted.sort((a, b) => a.title.localeCompare(b.title));
} else if (sortMode === 'author') {
sorted.sort((a, b) =>
(a.author || '').localeCompare(b.author || '') || a.title.localeCompare(b.title)
);
} else if (sortMode === 'newest') {
sorted.sort((a, b) => b.path.localeCompare(a.path));
} else {
sorted.sort((a, b) => a.order - b.order);
}
setFilteredBlueprints(sorted);
}, [searchTerm, activeCategories, blueprints, showFeaturedOnly, sortMode]);
const navItems = ['News', 'Showcase', 'Hosting', 'Extend', 'Learn', 'Community', 'About'];
const baseTabs = ['All', 'Featured'];
const tabItems = Array.from(new Set([...baseTabs, ...topCategories.slice(0, 6)]));
const handleTabClick = (tab) => {
if (tab === 'All') {
setActiveCategories(new Set());
setShowFeaturedOnly(false);
setSelectedFilter('all');
return;
}
if (tab === 'Featured') {
setActiveCategories(new Set());
setShowFeaturedOnly(true);
setSelectedFilter('featured');
return;
}
setActiveCategories(new Set([tab]));
setShowFeaturedOnly(false);
setSelectedFilter(tab);
};
const handleFilterChange = (value) => {
if (value === 'curated' || value === 'all') {
setSelectedFilter('all');
setActiveCategories(new Set());
setShowFeaturedOnly(false);
return;
}
if (value === 'featured') {
setSelectedFilter('featured');
setActiveCategories(new Set());
setShowFeaturedOnly(true);
return;
}
setSelectedFilter(value);
setActiveCategories(new Set([value]));
setShowFeaturedOnly(false);
};
const handleFeaturedToggle = (event) => {
const checked = event.target.checked;
setShowFeaturedOnly(checked);
if (checked) {
setSelectedFilter('featured');
setActiveCategories(new Set());
} else if (selectedFilter === 'featured') {
setSelectedFilter('all');
}
};
const showingAll = selectedFilter === 'all' &&
filteredBlueprints.length === blueprints.length &&
activeCategories.size === 0 &&
!searchTerm &&
!showFeaturedOnly;
const isTabActive = (tab) => {
if (tab === 'All') {
return selectedFilter === 'all';
}
if (tab === 'Featured') {
return selectedFilter === 'featured';
}
return activeCategories.has(tab);
};
const resultsMessage = !isLoaded
? 'Loading blueprints...'
: showingAll
? `Showing all ${formatNumber(blueprints.length)} blueprints`
: `Showing ${formatNumber(filteredBlueprints.length)} of ${formatNumber(blueprints.length)} blueprints`;
const filterOptions = [
{ value: 'all', label: 'All blueprints' },
{ value: 'featured', label: 'Featured' },
...allCategories.map(cat => ({ value: cat, label: cat }))
];
const placeholderCards = Array.from({ length: 6 }).map((_, index) =>
h(PlaceholderCard, { key: `placeholder-${index}` })
);
const shouldShowSkeletons = !isLoaded;
const showEmptyState = isLoaded && filteredBlueprints.length === 0;
const emptyState = h('div', { className: 'empty-state' },
h('h2', null, 'No blueprints match your filters'),
h('p', null, 'Try adjusting your search terms or selecting a different category.')
);
const galleryContent = shouldShowSkeletons
? h('div', { className: 'gallery-grid skeleton-grid' }, placeholderCards)
: showEmptyState
? emptyState
: h('div', { className: 'gallery-grid' },
filteredBlueprints.map(bp =>
h(BlueprintCard, {
blueprint: bp,
key: bp.path
})
)
);
return h('div', { className: 'page' },
h('section', { className: 'hero' },
h('div', { className: 'hero-inner' },
h('div', { className: 'hero-header' },
h('img', {
src: 'playground-icon.png',
alt: 'WordPress Playground',
className: 'hero-logo'
}),
h('h1', { className: 'hero-title' }, 'WordPress Blueprints Gallery')
),
h('p', { className: 'hero-subtitle' },
'Launch ready-made WordPress environments in seconds. Browse community-created ',
h('a', {
href: 'https://wordpress.github.io/wordpress-playground/',
target: '_blank',
rel: 'noreferrer noopener',
className: 'hero-link'
}, 'WordPress Playground'),
' blueprints and discover pre-configured setups. Learn how to ',
h('a', {
href: 'https://wordpress.github.io/wordpress-playground/blueprints/tutorial/',
target: '_blank',
rel: 'noreferrer noopener',
className: 'hero-link'
}, 'create your own')
)
)
),
h('section', { className: 'filters-section' },
h('div', { className: 'filters-inner' },
h('div', { className: 'tab-list' },
tabItems.map(tab =>
h('button', {
key: tab,
className: `tab ${isTabActive(tab) ? 'active' : ''}`,
onClick: () => handleTabClick(tab)
}, tab)
)
),
h('div', { className: 'toolbar' },
h('div', { className: 'search-block' },
h('div', { className: 'search-field' },
h('span', { className: 'search-icon' }, '⌕'),
h('input', {
type: 'search',
placeholder: 'Search blueprints',
value: searchTerm,
onInput: (event) => setSearchTerm(event.target.value)
})
)
)
)
)
),
h('main', { className: 'gallery-section' },
h('div', { className: 'gallery-inner' },
h('div', { className: 'results-summary' }, resultsMessage),
galleryContent
)
),
h('footer', { className: 'footer' },
h('p', null,
'Want to contribute your own blueprint? Check out the ',
h('a', {
href: 'https://github.com/wordpress/blueprints/blob/trunk/README.md#contributing-your-blueprint',
target: '_blank',
rel: 'noreferrer noopener'
}, 'contribution guidelines')
),
h('p', { style: { marginTop: '1rem' } },
h('a', { href: 'https://github.com/wordpress/blueprints', target: '_blank' }, 'View on GitHub'),
' • ',
h('a', { href: 'https://github.com/wordpress/blueprints/blob/trunk/GALLERY.md', target: '_blank' }, 'View as Markdown')
)
)
);
}
setTimeout(() => {
if (window.h && window.render) {
render(h(App), document.getElementById('app'));
}
}, 100);
</script>
</body>
</html>