From 0c2813bea6404bb89a0cbebdc065c778e5b73f2d Mon Sep 17 00:00:00 2001 From: Ender Date: Tue, 28 Oct 2025 12:43:17 +0100 Subject: [PATCH] feat: migrate stock photo integration from Unsplash to Pexels - Replaced Unsplash API integration with Pexels API for stock photo search and import - Updated API response transformation to match frontend expectations with Pexels data structure - Removed Unsplash-specific download tracking and attribution requirements - Modified environment configuration to use PEXELS_API_KEY instead of UNSPLASH_ACCESS_KEY - Updated UI text and attribution links to reference Pexels instead of Unsplash - Simplifie --- apps/admin/src/components/MediaLibrary.tsx | 8 +-- apps/api/src/routes/stock-photos.routes.ts | 63 +++++++++++++--------- docker-compose.yml | 3 ++ 3 files changed, 43 insertions(+), 31 deletions(-) diff --git a/apps/admin/src/components/MediaLibrary.tsx b/apps/admin/src/components/MediaLibrary.tsx index a499817..a4aee21 100644 --- a/apps/admin/src/components/MediaLibrary.tsx +++ b/apps/admin/src/components/MediaLibrary.tsx @@ -21,9 +21,6 @@ type StockPhoto = { html: string; }; }; - links: { - download_location: string; - }; width: number; height: number; }; @@ -198,7 +195,6 @@ export default function MediaLibrary({ body: JSON.stringify({ photoId: photo.id, downloadUrl: photo.urls.regular, - downloadLocation: photo.links.download_location, photographer: photo.user.name, photographerUrl: photo.user.links.html, }), @@ -298,7 +294,7 @@ export default function MediaLibrary({ {!stockLoading && stockPhotos.length === 0 && !stockQuery && ( - Enter a search term to find free stock photos from Unsplash. + Enter a search term to find free stock photos from Pexels. )} @@ -309,7 +305,7 @@ export default function MediaLibrary({ {photo.alt_description - Photo by {photo.user.name} + Photo by {photo.user.name} {photo.width} × {photo.height} diff --git a/apps/api/src/routes/stock-photos.routes.ts b/apps/api/src/routes/stock-photos.routes.ts index 0f1519d..3479a3a 100644 --- a/apps/api/src/routes/stock-photos.routes.ts +++ b/apps/api/src/routes/stock-photos.routes.ts @@ -4,11 +4,11 @@ import crypto from 'crypto'; const router = express.Router(); -const UNSPLASH_ACCESS_KEY = process.env.UNSPLASH_ACCESS_KEY; +const PEXELS_API_KEY = process.env.PEXELS_API_KEY; /** * GET /api/stock-photos/search - * Search Unsplash for stock photos + * Search Pexels for stock photos */ router.get('/search', async (req, res) => { try { @@ -18,21 +18,47 @@ router.get('/search', async (req, res) => { return res.status(400).json({ error: 'query parameter is required' }); } - if (!UNSPLASH_ACCESS_KEY) { - return res.status(500).json({ error: 'Unsplash API key not configured' }); + if (!PEXELS_API_KEY) { + return res.status(500).json({ error: 'Pexels API key not configured' }); } - const unsplashUrl = `https://api.unsplash.com/search/photos?query=${encodeURIComponent(query)}&per_page=${per_page}&client_id=${UNSPLASH_ACCESS_KEY}`; + const pexelsUrl = `https://api.pexels.com/v1/search?query=${encodeURIComponent(query)}&per_page=${per_page}`; - const response = await fetch(unsplashUrl); + const response = await fetch(pexelsUrl, { + headers: { + 'Authorization': PEXELS_API_KEY, + }, + }); if (!response.ok) { - throw new Error(`Unsplash API error: ${response.status}`); + throw new Error(`Pexels API error: ${response.status}`); } const data = await response.json(); - res.json(data); + // Transform Pexels response to match expected format + const transformedData = { + results: data.photos?.map((photo: any) => ({ + id: String(photo.id), + urls: { + small: photo.src.small, + regular: photo.src.large, + full: photo.src.original, + }, + alt_description: photo.alt || null, + user: { + name: photo.photographer, + links: { + html: photo.photographer_url, + }, + }, + width: photo.width, + height: photo.height, + })) || [], + total: data.total_results || 0, + }; + + res.json(transformedData); } catch (err: any) { console.error('[Stock Photos] Search error:', err); res.status(500).json({ @@ -48,31 +74,18 @@ router.get('/search', async (req, res) => { */ router.post('/import', async (req, res) => { try { - const { photoId, downloadUrl, downloadLocation, photographer, photographerUrl } = req.body; + const { photoId, downloadUrl, photographer, photographerUrl } = req.body; if (!photoId || !downloadUrl) { return res.status(400).json({ error: 'photoId and downloadUrl are required' }); } - if (!UNSPLASH_ACCESS_KEY) { - return res.status(500).json({ error: 'Unsplash API key not configured' }); + if (!PEXELS_API_KEY) { + return res.status(500).json({ error: 'Pexels API key not configured' }); } console.log('[Stock Photos] Importing photo:', photoId); - // Trigger download tracking (required by Unsplash API guidelines) - if (downloadLocation) { - try { - await fetch(downloadLocation, { - headers: { - 'Authorization': `Client-ID ${UNSPLASH_ACCESS_KEY}`, - }, - }); - } catch (err) { - console.warn('[Stock Photos] Failed to track download:', err); - } - } - // Download the image const imageResponse = await fetch(downloadUrl); @@ -93,7 +106,7 @@ router.post('/import', async (req, res) => { // Generate unique filename const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD const randomId = crypto.randomBytes(8).toString('hex'); - const filename = `unsplash-${photoId}-${randomId}.${ext}`; + const filename = `pexels-${photoId}-${randomId}.${ext}`; const key = `images/${timestamp}/${filename}`; // Upload to S3 diff --git a/docker-compose.yml b/docker-compose.yml index 20a2c60..33e4367 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -49,6 +49,9 @@ services: S3_ACCESS_KEY: ${S3_ACCESS_KEY} S3_SECRET_KEY: ${S3_SECRET_KEY} S3_ENDPOINT: ${S3_ENDPOINT} + INFISICAL_TOKEN: ${INFISICAL_TOKEN} + INFISICAL_SITE_URL: ${INFISICAL_SITE_URL} + PEXELS_API_KEY: ${PEXELS_API_KEY} depends_on: mysql: condition: service_healthy