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
This commit is contained in:
Ender 2025-10-28 12:43:17 +01:00
parent 02878439af
commit 0c2813bea6
3 changed files with 43 additions and 31 deletions

View File

@ -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 && (
<Typography variant="body2" sx={{ textAlign: 'center', py: 4, color: 'text.secondary' }}>
Enter a search term to find free stock photos from Unsplash.
Enter a search term to find free stock photos from Pexels.
</Typography>
)}
@ -309,7 +305,7 @@ export default function MediaLibrary({
<img src={photo.urls.small} alt={photo.alt_description || 'Stock photo'} style={{ maxWidth: '100%', maxHeight: '150px', objectFit: 'contain', display: 'block' }} />
</Box>
<Typography variant="caption" sx={{ display: 'block', mb: 0.5 }}>
Photo by <a href={photo.user.links.html + '?utm_source=voxblog&utm_medium=referral'} target="_blank" rel="noopener noreferrer" style={{ color: 'inherit' }}>{photo.user.name}</a>
Photo by <a href={photo.user.links.html} target="_blank" rel="noopener noreferrer" style={{ color: 'inherit' }}>{photo.user.name}</a>
</Typography>
<Typography variant="caption" sx={{ display: 'block', color: 'text.secondary', mb: 1 }}>
{photo.width} × {photo.height}

View File

@ -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

View File

@ -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