Files
Critters/lib/critter-modal.php
T
pauljd 9805d5056d
pauljd/davidson-ci: Davidson CI / quality (push) Successful in 14s
pauljd/davidson-ci: Davidson CI / ai-review (push) Successful in 1m5s
issues identified by Davidson CI
2026-09-09 22:49:46 +01:00

260 lines
15 KiB
PHP

<div id="critterModalOverlay" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
<div class="relative w-full max-w-4xl max-h-[90vh] overflow-y-auto rounded-3xl bg-white shadow-2xl grid md:grid-cols-2">
<button type="button" onclick="closeCritterModal()" aria-label="Close" class="absolute top-4 right-4 z-10 flex h-9 w-9 items-center justify-center rounded-full bg-white/90 shadow hover:bg-white text-gray-500">
<i data-lucide="x" class="h-5 w-5"></i>
</button>
<div class="p-6 bg-gradient-to-br from-pink-50 via-white to-amber-50">
<div class="flex items-start justify-between mb-4 pr-10">
<div>
<h2 id="critterModalTitleLeft" class="text-2xl font-extrabold text-gray-900"></h2>
<p id="critterModalIdLeft" class="text-sm text-gray-400"></p>
</div>
<span id="critterModalRarityLeft" class="inline-flex items-center gap-1 rounded-full text-xs font-semibold px-3 py-1 shrink-0"></span>
</div>
<div class="rounded-2xl overflow-hidden ring-1 ring-gray-100 shadow-sm">
<img id="critterModalImage" src="" alt="" class="w-full aspect-square object-cover">
</div>
<div class="mt-4 grid grid-cols-4 gap-2 text-center">
<div>
<div class="mx-auto flex h-9 w-9 items-center justify-center rounded-full bg-pink-100 text-pink-600 mb-1"><i data-lucide="wand" class="h-4 w-4"></i></div>
<p id="critterModalAdjective" class="text-xs font-semibold text-gray-800 truncate"></p>
<p class="text-[11px] text-gray-400">Adjective</p>
</div>
<div>
<div class="mx-auto flex h-9 w-9 items-center justify-center rounded-full bg-blue-100 text-blue-600 mb-1"><i data-lucide="paw-print" class="h-4 w-4"></i></div>
<p id="critterModalNoun" class="text-xs font-semibold text-gray-800 truncate"></p>
<p class="text-[11px] text-gray-400">Animal</p>
</div>
<div>
<div class="mx-auto flex h-9 w-9 items-center justify-center rounded-full bg-green-100 text-green-600 mb-1"><i data-lucide="play" class="h-4 w-4"></i></div>
<p id="critterModalVerb" class="text-xs font-semibold text-gray-800 truncate"></p>
<p class="text-[11px] text-gray-400">Action</p>
</div>
<div>
<div class="mx-auto flex h-9 w-9 items-center justify-center rounded-full bg-purple-100 text-purple-600 mb-1"><i data-lucide="map-pin" class="h-4 w-4"></i></div>
<p id="critterModalLocation" class="text-xs font-semibold text-gray-800 truncate"></p>
<p class="text-[11px] text-gray-400">Location</p>
</div>
</div>
<p id="critterModalFlourish" class="mt-4 text-center italic text-sm text-pink-600"></p>
</div>
<div class="p-6">
<h2 id="critterModalTitleRight" class="text-2xl font-extrabold text-gray-900 pr-10"></h2>
<div class="mt-1 flex items-center gap-3">
<span id="critterModalIdRight" class="text-sm text-gray-400"></span>
<span id="critterModalRarityRight" class="inline-flex items-center gap-1 rounded-full text-xs font-semibold px-3 py-1"></span>
<button type="button" id="critterModalFavoriteBtn" onclick="toggleModalFavorite()" aria-label="Toggle favorite" class="ml-auto flex h-9 w-9 items-center justify-center rounded-full ring-1 ring-gray-200 text-gray-400 hover:bg-gray-50">
<i data-lucide="heart" class="h-4 w-4"></i>
</button>
</div>
<p id="critterModalDescription" class="mt-4 text-sm text-gray-600 leading-relaxed"></p>
<div class="mt-4 divide-y divide-gray-100 border-y border-gray-100">
<div class="flex items-center gap-2 py-2">
<i data-lucide="wand" class="h-4 w-4 text-pink-500 shrink-0"></i>
<span class="text-sm text-gray-500 w-20 shrink-0">Adjective</span>
<span id="critterModalAdjectivePill" class="inline-flex items-center rounded-full bg-pink-50 text-pink-700 text-xs font-medium px-2.5 py-1"></span>
</div>
<div class="flex items-center gap-2 py-2">
<i data-lucide="paw-print" class="h-4 w-4 text-blue-500 shrink-0"></i>
<span class="text-sm text-gray-500 w-20 shrink-0">Animal</span>
<span id="critterModalNounPill" class="inline-flex items-center rounded-full bg-blue-50 text-blue-700 text-xs font-medium px-2.5 py-1"></span>
</div>
<div class="flex items-center gap-2 py-2">
<i data-lucide="play" class="h-4 w-4 text-green-500 shrink-0"></i>
<span class="text-sm text-gray-500 w-20 shrink-0">Action</span>
<span id="critterModalVerbPill" class="inline-flex items-center rounded-full bg-green-50 text-green-700 text-xs font-medium px-2.5 py-1"></span>
</div>
<div class="flex items-center gap-2 py-2">
<i data-lucide="map-pin" class="h-4 w-4 text-purple-500 shrink-0"></i>
<span class="text-sm text-gray-500 w-20 shrink-0">Location</span>
<span id="critterModalLocationPill" class="inline-flex items-center rounded-full bg-purple-50 text-purple-700 text-xs font-medium px-2.5 py-1"></span>
</div>
</div>
<div class="mt-4 grid grid-cols-3 gap-3 text-xs text-gray-500">
<div class="flex items-center gap-1.5">
<i data-lucide="calendar" class="h-4 w-4 shrink-0"></i>
<div>
<p class="text-gray-400">Created</p>
<p id="critterModalCreated" class="text-gray-700 font-medium"></p>
</div>
</div>
<div class="flex items-center gap-1.5">
<i data-lucide="scaling" class="h-4 w-4 shrink-0"></i>
<div>
<p class="text-gray-400">Resolution</p>
<p id="critterModalResolution" class="text-gray-700 font-medium"></p>
</div>
</div>
<div class="flex items-center gap-1.5">
<i data-lucide="hard-drive" class="h-4 w-4 shrink-0"></i>
<div>
<p class="text-gray-400">File Size</p>
<p id="critterModalFileSize" class="text-gray-700 font-medium"></p>
</div>
</div>
</div>
<div class="mt-4 rounded-xl bg-blue-50 ring-1 ring-blue-100 p-3">
<div class="flex items-center justify-between mb-1">
<span class="inline-flex items-center gap-1.5 text-xs font-semibold text-blue-700">
<i data-lucide="sparkles" class="h-4 w-4"></i> Prompt Used
</span>
<button type="button" onclick="copyModalPrompt()" aria-label="Copy prompt" class="text-blue-400 hover:text-blue-600">
<i data-lucide="copy" class="h-4 w-4"></i>
</button>
</div>
<p id="critterModalPrompt" class="text-xs italic text-gray-600 leading-relaxed"></p>
</div>
<div class="mt-4 grid grid-cols-3 gap-2">
<a id="critterModalDownloadBtn" href="#" download class="flex items-center justify-center gap-1.5 rounded-xl bg-gray-900 text-white text-sm font-semibold py-2.5 hover:bg-gray-800">
<i data-lucide="download" class="h-4 w-4"></i> Download
</a>
<button type="button" id="critterModalShareBtn" onclick="shareModalCritter()" class="flex items-center justify-center gap-1.5 rounded-xl bg-gray-100 text-gray-700 text-sm font-semibold py-2.5 hover:bg-gray-200">
<i data-lucide="share-2" class="h-4 w-4"></i> Share
</button>
<a id="critterModalCreateSimilarBtn" href="#" class="flex items-center justify-center gap-1.5 rounded-xl bg-gray-100 text-gray-700 text-sm font-semibold py-2.5 hover:bg-gray-200">
<i data-lucide="refresh-cw" class="h-4 w-4"></i> Create Similar
</a>
</div>
</div>
</div>
</div>
<script>
let currentCritterModalId = null;
function updateModalFavoriteButton(isFavorite) {
const btn = document.getElementById('critterModalFavoriteBtn');
const icon = btn.querySelector('[data-lucide]');
icon.classList.toggle('text-pink-500', isFavorite);
icon.classList.toggle('fill-pink-500', isFavorite);
btn.classList.toggle('ring-pink-200', isFavorite);
}
function renderCritterModal(data) {
currentCritterModalId = data.id;
const idText = '#' + String(data.id).padStart(4, '0');
document.getElementById('critterModalTitleLeft').textContent = data.title;
document.getElementById('critterModalTitleRight').textContent = data.title;
document.getElementById('critterModalIdLeft').textContent = idText;
document.getElementById('critterModalIdRight').textContent = idText;
const rarityClasses = 'inline-flex items-center gap-1 rounded-full text-xs font-semibold px-3 py-1 shrink-0 ' + data.rarity.classes;
const rarityLeft = document.getElementById('critterModalRarityLeft');
const rarityRight = document.getElementById('critterModalRarityRight');
rarityLeft.textContent = data.rarity.label;
rarityRight.textContent = data.rarity.label;
rarityLeft.className = rarityClasses;
rarityRight.className = rarityClasses.replace(' shrink-0', '');
const img = document.getElementById('critterModalImage');
img.src = data.imageUrl || '';
img.alt = data.title;
document.getElementById('critterModalAdjective').textContent = data.adjective;
document.getElementById('critterModalNoun').textContent = data.noun;
document.getElementById('critterModalVerb').textContent = data.verb;
document.getElementById('critterModalLocation').textContent = data.location;
document.getElementById('critterModalAdjectivePill').textContent = data.adjective;
document.getElementById('critterModalNounPill').textContent = data.noun;
document.getElementById('critterModalVerbPill').textContent = data.verb;
document.getElementById('critterModalLocationPill').textContent = data.location;
const sentences = data.description.split('. ');
document.getElementById('critterModalFlourish').textContent = '"' + sentences[sentences.length - 1].replace(/\.$/, '') + '"';
document.getElementById('critterModalDescription').textContent = data.description;
document.getElementById('critterModalCreated').textContent = data.createdAt || '—';
document.getElementById('critterModalResolution').textContent = data.resolution || '—';
document.getElementById('critterModalFileSize').textContent = data.fileSize || '—';
document.getElementById('critterModalPrompt').textContent = data.prompt;
document.getElementById('critterModalDownloadBtn').href = data.imageUrl || '#';
document.getElementById('critterModalCreateSimilarBtn').href = data.createSimilarUrl;
updateModalFavoriteButton(data.isFavorite);
window.__critterModalShareUrl = window.location.origin + '/' + data.shareUrl;
window.__critterModalPromptText = data.prompt;
}
function openCritterModal(id) {
fetch('api/critter-details.php?id=' + encodeURIComponent(id))
.then((res) => res.json())
.then((data) => {
if (data.error) return;
renderCritterModal(data);
document.getElementById('critterModalOverlay').classList.remove('hidden');
if (window.lucide) lucide.createIcons();
});
}
function closeCritterModal() {
document.getElementById('critterModalOverlay').classList.add('hidden');
}
document.getElementById('critterModalOverlay').addEventListener('click', function (event) {
if (event.target === this) closeCritterModal();
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape') closeCritterModal();
});
function getCsrfToken() {
const meta = document.querySelector('meta[name="csrf-token"]');
return meta ? meta.content : '';
}
function toggleModalFavorite() {
if (!currentCritterModalId) return;
fetch('api/critter-details.php', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ id: currentCritterModalId, action: 'toggle_favorite', csrf_token: getCsrfToken() }),
})
.then((res) => res.json())
.then((data) => {
if (data.error) return;
updateModalFavoriteButton(data.isFavorite);
const card = document.querySelector('[data-critter-card="' + currentCritterModalId + '"]');
if (card) {
card.querySelectorAll('[data-favorite-icon]').forEach((icon) => {
icon.classList.toggle('text-pink-500', data.isFavorite);
icon.classList.toggle('fill-pink-500', data.isFavorite);
});
}
});
}
function copyModalPrompt() {
if (window.__critterModalPromptText && navigator.clipboard) {
navigator.clipboard.writeText(window.__critterModalPromptText);
}
}
function shareModalCritter() {
const url = window.__critterModalShareUrl;
if (navigator.share) {
navigator.share({ title: 'Check out my critter!', url }).catch(() => {});
return;
}
if (navigator.clipboard) {
navigator.clipboard.writeText(url);
const btn = document.getElementById('critterModalShareBtn');
const original = btn.innerHTML;
btn.textContent = 'Link copied!';
setTimeout(() => {
btn.innerHTML = original;
if (window.lucide) lucide.createIcons();
}, 1500);
}
}
</script>