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:
@@ -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': '*'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user