update .gitignore, enhance README, implement Pexels API integration, and improve page generation logic
This commit is contained in:
220
web/index.php
220
web/index.php
@@ -2,6 +2,70 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
function collectSeedPages(): array
|
||||
{
|
||||
$pageFiles = glob(__DIR__ . '/../pages/seed-*.html') ?: [];
|
||||
usort($pageFiles, static fn (string $a, string $b): int => filemtime($b) <=> filemtime($a));
|
||||
|
||||
$items = [];
|
||||
foreach ($pageFiles as $pageFile) {
|
||||
$seed = basename($pageFile, '.html');
|
||||
$previewWebPath = '/previews/' . $seed . '.jpg';
|
||||
$previewFsPath = __DIR__ . '/previews/' . $seed . '.jpg';
|
||||
$hasPreview = file_exists($previewFsPath);
|
||||
|
||||
$title = $seed;
|
||||
$rawHtml = file_get_contents($pageFile);
|
||||
if ($rawHtml !== false && preg_match('/<title>(.*?)<\/title>/is', $rawHtml, $matches) === 1) {
|
||||
$title = trim(strip_tags($matches[1]));
|
||||
}
|
||||
|
||||
$items[] = [
|
||||
'seed' => $seed,
|
||||
'title' => $title,
|
||||
'updated' => date('Y-m-d H:i:s', filemtime($pageFile)),
|
||||
'hasPreview' => $hasPreview,
|
||||
'previewWebPath' => $previewWebPath,
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
function renderSeedCards(array $items): string
|
||||
{
|
||||
if ($items === []) {
|
||||
return '<p class="text-zinc-300">No generated seed pages found yet.</p>';
|
||||
}
|
||||
|
||||
$html = '<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-6">';
|
||||
foreach ($items as $item) {
|
||||
$seed = htmlspecialchars((string) $item['seed'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
$title = htmlspecialchars((string) $item['title'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
$updated = htmlspecialchars((string) $item['updated'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
$previewWebPath = htmlspecialchars((string) $item['previewWebPath'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
$hasPreview = (bool) $item['hasPreview'];
|
||||
|
||||
$html .= '<a href="/' . $seed . '" class="block rounded-xl overflow-hidden border border-zinc-800 bg-zinc-900/70 hover:border-emerald-500 transition">';
|
||||
$html .= '<div class="aspect-[16/10] bg-zinc-800">';
|
||||
if ($hasPreview) {
|
||||
$html .= '<img loading="lazy" src="' . $previewWebPath . '" alt="Preview for ' . $seed . '" class="w-full h-full object-cover">';
|
||||
} else {
|
||||
$html .= '<div class="w-full h-full grid place-items-center text-zinc-400 text-sm">Preview not available yet</div>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
$html .= '<div class="p-4">';
|
||||
$html .= '<h2 class="text-lg font-semibold mb-1 line-clamp-2">' . $title . '</h2>';
|
||||
$html .= '<p class="text-xs text-zinc-400 mb-1">' . $seed . '</p>';
|
||||
$html .= '<p class="text-xs text-zinc-500">Updated: ' . $updated . '</p>';
|
||||
$html .= '</div></a>';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
$path = (string) parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
|
||||
$segments = array_values(array_filter(explode('/', ltrim($path, '/'))));
|
||||
$firstSegment = $segments[0] ?? '';
|
||||
@@ -24,11 +88,15 @@ if ($firstSegment === 'generate') {
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($firstSegment === 'list') {
|
||||
if ($firstSegment === 'list-cards') {
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
echo renderSeedCards(collectSeedPages());
|
||||
exit;
|
||||
}
|
||||
|
||||
$pageFiles = glob(__DIR__ . '/../pages/seed-*.html') ?: [];
|
||||
usort($pageFiles, static fn (string $a, string $b): int => filemtime($b) <=> filemtime($a));
|
||||
if ($firstSegment === '' || $firstSegment === 'list') {
|
||||
header('Content-Type: text/html; charset=UTF-8');
|
||||
$cardsHtml = renderSeedCards(collectSeedPages());
|
||||
|
||||
echo '<!doctype html><html lang="en"><head><meta charset="UTF-8">';
|
||||
echo '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
|
||||
@@ -36,98 +104,76 @@ if ($firstSegment === 'list') {
|
||||
echo '<script src="https://cdn.tailwindcss.com"></script></head>';
|
||||
echo '<body class="min-h-screen bg-zinc-950 text-zinc-100 p-6 md:p-10">';
|
||||
echo '<div class="max-w-7xl mx-auto">';
|
||||
echo '<div class="flex flex-wrap items-center justify-between gap-4 mb-8">';
|
||||
echo '<div class="flex flex-wrap items-center justify-between gap-4 mb-3">';
|
||||
echo '<h1 class="text-3xl md:text-4xl font-bold">Generated Seeds</h1>';
|
||||
echo '<a href="/" class="px-4 py-2 rounded-md bg-emerald-600 hover:bg-emerald-500 transition text-white font-medium">Generate New</a>';
|
||||
echo '<div class="flex flex-wrap items-center gap-3">';
|
||||
echo '<button id="generate-btn" type="button" class="px-4 py-2 rounded-md bg-emerald-600 hover:bg-emerald-500 transition text-white font-medium">Generate New</button>';
|
||||
echo '<label class="inline-flex items-center gap-2 text-sm text-zinc-200 select-none">';
|
||||
echo '<input id="auto-gen-toggle" type="checkbox" class="h-4 w-4 accent-emerald-500">';
|
||||
echo '<span>Auto Gen</span>';
|
||||
echo '</label>';
|
||||
echo '</div>';
|
||||
echo '</div>';
|
||||
echo '<p id="generation-status" class="text-sm text-zinc-400 mb-6">Idle.</p>';
|
||||
echo '<div id="seed-cards">' . $cardsHtml . '</div>';
|
||||
echo '</div>';
|
||||
|
||||
if ($pageFiles === []) {
|
||||
echo '<p class="text-zinc-300">No generated seed pages found yet.</p>';
|
||||
} else {
|
||||
echo '<div class="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-6">';
|
||||
echo '<script>';
|
||||
echo '(function () {';
|
||||
echo 'const generateBtn = document.getElementById("generate-btn");';
|
||||
echo 'const autoToggle = document.getElementById("auto-gen-toggle");';
|
||||
echo 'const statusEl = document.getElementById("generation-status");';
|
||||
echo 'const cardsEl = document.getElementById("seed-cards");';
|
||||
echo 'let inFlight = false;';
|
||||
|
||||
foreach ($pageFiles as $pageFile) {
|
||||
$seed = basename($pageFile, '.html');
|
||||
$previewWebPath = '/previews/' . $seed . '.jpg';
|
||||
$previewFsPath = __DIR__ . '/previews/' . $seed . '.jpg';
|
||||
$hasPreview = file_exists($previewFsPath);
|
||||
echo 'async function refreshCards() {';
|
||||
echo ' const res = await fetch("/list-cards", { cache: "no-store" });';
|
||||
echo ' if (!res.ok) throw new Error("Could not refresh list");';
|
||||
echo ' cardsEl.innerHTML = await res.text();';
|
||||
echo '}';
|
||||
|
||||
$title = $seed;
|
||||
$rawHtml = file_get_contents($pageFile);
|
||||
if ($rawHtml !== false && preg_match('/<title>(.*?)<\/title>/is', $rawHtml, $matches) === 1) {
|
||||
$title = trim(strip_tags($matches[1]));
|
||||
}
|
||||
echo 'async function runGeneration() {';
|
||||
echo ' if (inFlight) return;';
|
||||
echo ' inFlight = true;';
|
||||
echo ' generateBtn.disabled = true;';
|
||||
echo ' generateBtn.classList.add("opacity-60", "cursor-not-allowed");';
|
||||
echo ' statusEl.textContent = "Generating new page...";';
|
||||
echo ' try {';
|
||||
echo ' const res = await fetch("/generate", { cache: "no-store" });';
|
||||
echo ' if (!res.ok) throw new Error("Generation failed");';
|
||||
echo ' const redirectTo = res.headers.get("HX-Redirect") || "";';
|
||||
echo ' await refreshCards();';
|
||||
echo ' statusEl.textContent = redirectTo ? ("Done: " + redirectTo + " (list updated)") : "Done (list updated)";';
|
||||
echo ' } catch (err) {';
|
||||
echo ' statusEl.textContent = "Generation failed. Check backend logs.";';
|
||||
echo ' autoToggle.checked = false;';
|
||||
echo ' } finally {';
|
||||
echo ' inFlight = false;';
|
||||
echo ' generateBtn.disabled = false;';
|
||||
echo ' generateBtn.classList.remove("opacity-60", "cursor-not-allowed");';
|
||||
echo ' if (autoToggle.checked) {';
|
||||
echo ' statusEl.textContent = "Auto Gen enabled. Starting next generation...";';
|
||||
echo ' setTimeout(function () { runGeneration(); }, 800);';
|
||||
echo ' }';
|
||||
echo ' }';
|
||||
echo '}';
|
||||
|
||||
$updated = date('Y-m-d H:i:s', filemtime($pageFile));
|
||||
echo 'generateBtn.addEventListener("click", function () { runGeneration(); });';
|
||||
echo 'autoToggle.addEventListener("change", function () {';
|
||||
echo ' if (autoToggle.checked) {';
|
||||
echo ' statusEl.textContent = inFlight ? "Auto Gen enabled. Waiting for current run..." : "Auto Gen enabled. Starting generation...";';
|
||||
echo ' if (!inFlight) runGeneration();';
|
||||
echo ' } else {';
|
||||
echo ' statusEl.textContent = inFlight ? "Auto Gen disabled. Current run will finish." : "Auto Gen disabled.";';
|
||||
echo ' }';
|
||||
echo '});';
|
||||
|
||||
echo '<a href="/' . htmlspecialchars($seed, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '" class="block rounded-xl overflow-hidden border border-zinc-800 bg-zinc-900/70 hover:border-emerald-500 transition">';
|
||||
echo '<div class="aspect-[16/10] bg-zinc-800">';
|
||||
if ($hasPreview) {
|
||||
echo '<img loading="lazy" src="' . htmlspecialchars($previewWebPath, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '" alt="Preview for ' . htmlspecialchars($seed, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '" class="w-full h-full object-cover">';
|
||||
} else {
|
||||
echo '<div class="w-full h-full grid place-items-center text-zinc-400 text-sm">Preview not available yet</div>';
|
||||
}
|
||||
echo '</div>';
|
||||
echo '<div class="p-4">';
|
||||
echo '<h2 class="text-lg font-semibold mb-1 line-clamp-2">' . htmlspecialchars($title, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</h2>';
|
||||
echo '<p class="text-xs text-zinc-400 mb-1">' . htmlspecialchars($seed, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</p>';
|
||||
echo '<p class="text-xs text-zinc-500">Updated: ' . htmlspecialchars($updated, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '</p>';
|
||||
echo '</div></a>';
|
||||
}
|
||||
echo '})();';
|
||||
echo '</script>';
|
||||
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
echo '</div></body></html>';
|
||||
echo '</body></html>';
|
||||
exit;
|
||||
}
|
||||
|
||||
?>
|
||||
|
||||
|
||||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Generation Error</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.10/dist/htmx.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body class="min-h-screen bg-zinc-950 text-zinc-100 p-8" hx-get="/generate" hx-trigger="load">
|
||||
<h1 class="text-2xl font-bold mb-4">Page being generated, please wait</h1>
|
||||
<p class="text-zinc-300 mb-3">
|
||||
Generating content... this should complete in about 30 seconds.
|
||||
</p>
|
||||
<div class="w-full max-w-xl h-3 bg-zinc-800 rounded overflow-hidden mb-2" aria-label="Generation progress">
|
||||
<div id="progress-bar" class="h-full bg-emerald-500 w-0 transition-[width] duration-100"></div>
|
||||
</div>
|
||||
<p id="progress-text" class="text-zinc-400 text-sm mb-3">0%</p>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const durationMs = 30000;
|
||||
const start = performance.now();
|
||||
const bar = document.getElementById('progress-bar');
|
||||
const text = document.getElementById('progress-text');
|
||||
|
||||
function tick(now) {
|
||||
const elapsed = now - start;
|
||||
const progress = Math.min(elapsed / durationMs, 1);
|
||||
const percent = Math.round(progress * 100);
|
||||
|
||||
bar.style.width = percent + '%';
|
||||
text.textContent = percent + '%';
|
||||
|
||||
if (progress < 1) {
|
||||
requestAnimationFrame(tick);
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(tick);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
http_response_code(404);
|
||||
echo 'Not found';
|
||||
|
||||
Reference in New Issue
Block a user