feat: add embeddable pet widget endpoint

Serves a self-contained JavaScript widget at /embed/pets.js that renders available pets in a Shadow DOM grid. Supports data attributes for limit, species, columns, and theme. CORS-enabled for embedding on partner sites.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-26 16:19:23 -05:00
co-authored by Claude Sonnet 4.6
parent 51adec43e0
commit 6307d1b515
+104
View File
@@ -0,0 +1,104 @@
import type { RequestHandler } from './$types';
import { db } from '$lib/server/db';
import { pets, petPhotos } from '$lib/server/schema';
import { eq, and } from 'drizzle-orm';
export const GET: RequestHandler = async ({ url }) => {
// Fetch available pets with primary photos
const rows = await db
.select({
id: pets.id,
name: pets.name,
slug: pets.slug,
breed: pets.breed,
age: pets.age,
sex: pets.sex,
species: pets.species,
photoUrl: petPhotos.url
})
.from(pets)
.leftJoin(petPhotos, and(eq(petPhotos.petId, pets.id), eq(petPhotos.isPrimary, true)))
.where(and(eq(pets.status, 'available'), eq(pets.published, true)))
.limit(20);
const petsJson = JSON.stringify(rows);
const siteUrl = process.env.PUBLIC_SITE_URL || url.origin;
const js = `
(function() {
var container = document.getElementById('fosterflow-pets');
if (!container) { console.warn('FosterFlow: #fosterflow-pets not found'); return; }
var limit = parseInt(container.dataset.limit || '12');
var species = container.dataset.species || '';
var columns = parseInt(container.dataset.columns || '3');
var theme = container.dataset.theme || 'light';
var pets = ${petsJson};
if (species) pets = pets.filter(function(p) { return p.species === species; });
pets = pets.slice(0, limit);
var shadow = container.attachShadow({ mode: 'open' });
var style = document.createElement('style');
style.textContent = \`
:host { display: block; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; }
.ff-grid { display: grid; grid-template-columns: repeat(\${columns}, 1fr); gap: 16px; }
@media (max-width: 768px) { .ff-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 480px) { .ff-grid { grid-template-columns: 1fr; } }
.ff-card { border-radius: 12px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,0.1); transition: box-shadow 0.2s; background: \${theme === 'dark' ? '#1f2937' : '#fff'}; }
.ff-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.15); }
.ff-card a { text-decoration: none; color: inherit; display: block; }
.ff-img { aspect-ratio: 4/3; background: \${theme === 'dark' ? '#374151' : '#f3f4f6'}; overflow: hidden; display: flex; align-items: center; justify-content: center; }
.ff-img img { width: 100%; height: 100%; object-fit: cover; }
.ff-img .ff-placeholder { font-size: 2rem; }
.ff-info { padding: 12px; }
.ff-name { font-weight: 700; font-size: 1rem; color: \${theme === 'dark' ? '#f9fafb' : '#111827'}; margin: 0 0 4px; }
.ff-meta { font-size: 0.8rem; color: \${theme === 'dark' ? '#9ca3af' : '#6b7280'}; margin: 0; }
.ff-footer { text-align: center; padding: 12px; font-size: 0.75rem; color: #9ca3af; }
.ff-footer a { color: #0d9488; text-decoration: none; }
.ff-empty { text-align: center; padding: 24px; color: #9ca3af; }
\`;
shadow.appendChild(style);
var wrapper = document.createElement('div');
if (pets.length === 0) {
wrapper.innerHTML = '<div class="ff-empty">No pets available at the moment.</div>';
} else {
var grid = document.createElement('div');
grid.className = 'ff-grid';
pets.forEach(function(pet) {
var card = document.createElement('div');
card.className = 'ff-card';
var photoHtml = pet.photoUrl
? '<img src="' + '${siteUrl}' + pet.photoUrl + '" alt="' + pet.name + '">'
: '<span class="ff-placeholder">🐾</span>';
card.innerHTML = '<a href="${siteUrl}/pets/' + pet.slug + '" target="_blank">' +
'<div class="ff-img">' + photoHtml + '</div>' +
'<div class="ff-info">' +
'<p class="ff-name">' + pet.name + '</p>' +
'<p class="ff-meta">' + (pet.breed || pet.species) + (pet.age ? ' · ' + pet.age : '') + ' · ' + pet.sex + '</p>' +
'</div></a>';
grid.appendChild(card);
});
wrapper.appendChild(grid);
}
var footer = document.createElement('div');
footer.className = 'ff-footer';
footer.innerHTML = 'Powered by <a href="https://fosterflow.app" target="_blank">FosterFlow</a>';
wrapper.appendChild(footer);
shadow.appendChild(wrapper);
})();
`;
return new Response(js, {
headers: {
'Content-Type': 'application/javascript',
'Cache-Control': 'public, max-age=300',
'Access-Control-Allow-Origin': '*'
}
});
};