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:
parent
02878439af
commit
0c2813bea6
@ -21,9 +21,6 @@ type StockPhoto = {
|
|||||||
html: string;
|
html: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
links: {
|
|
||||||
download_location: string;
|
|
||||||
};
|
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
@ -198,7 +195,6 @@ export default function MediaLibrary({
|
|||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
photoId: photo.id,
|
photoId: photo.id,
|
||||||
downloadUrl: photo.urls.regular,
|
downloadUrl: photo.urls.regular,
|
||||||
downloadLocation: photo.links.download_location,
|
|
||||||
photographer: photo.user.name,
|
photographer: photo.user.name,
|
||||||
photographerUrl: photo.user.links.html,
|
photographerUrl: photo.user.links.html,
|
||||||
}),
|
}),
|
||||||
@ -298,7 +294,7 @@ export default function MediaLibrary({
|
|||||||
|
|
||||||
{!stockLoading && stockPhotos.length === 0 && !stockQuery && (
|
{!stockLoading && stockPhotos.length === 0 && !stockQuery && (
|
||||||
<Typography variant="body2" sx={{ textAlign: 'center', py: 4, color: 'text.secondary' }}>
|
<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>
|
</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' }} />
|
<img src={photo.urls.small} alt={photo.alt_description || 'Stock photo'} style={{ maxWidth: '100%', maxHeight: '150px', objectFit: 'contain', display: 'block' }} />
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="caption" sx={{ display: 'block', mb: 0.5 }}>
|
<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>
|
||||||
<Typography variant="caption" sx={{ display: 'block', color: 'text.secondary', mb: 1 }}>
|
<Typography variant="caption" sx={{ display: 'block', color: 'text.secondary', mb: 1 }}>
|
||||||
{photo.width} × {photo.height}
|
{photo.width} × {photo.height}
|
||||||
|
|||||||
@ -4,11 +4,11 @@ import crypto from 'crypto';
|
|||||||
|
|
||||||
const router = express.Router();
|
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
|
* GET /api/stock-photos/search
|
||||||
* Search Unsplash for stock photos
|
* Search Pexels for stock photos
|
||||||
*/
|
*/
|
||||||
router.get('/search', async (req, res) => {
|
router.get('/search', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@ -18,21 +18,47 @@ router.get('/search', async (req, res) => {
|
|||||||
return res.status(400).json({ error: 'query parameter is required' });
|
return res.status(400).json({ error: 'query parameter is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!UNSPLASH_ACCESS_KEY) {
|
if (!PEXELS_API_KEY) {
|
||||||
return res.status(500).json({ error: 'Unsplash API key not configured' });
|
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) {
|
if (!response.ok) {
|
||||||
throw new Error(`Unsplash API error: ${response.status}`);
|
throw new Error(`Pexels API error: ${response.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
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) {
|
} catch (err: any) {
|
||||||
console.error('[Stock Photos] Search error:', err);
|
console.error('[Stock Photos] Search error:', err);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
@ -48,31 +74,18 @@ router.get('/search', async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
router.post('/import', async (req, res) => {
|
router.post('/import', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { photoId, downloadUrl, downloadLocation, photographer, photographerUrl } = req.body;
|
const { photoId, downloadUrl, photographer, photographerUrl } = req.body;
|
||||||
|
|
||||||
if (!photoId || !downloadUrl) {
|
if (!photoId || !downloadUrl) {
|
||||||
return res.status(400).json({ error: 'photoId and downloadUrl are required' });
|
return res.status(400).json({ error: 'photoId and downloadUrl are required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!UNSPLASH_ACCESS_KEY) {
|
if (!PEXELS_API_KEY) {
|
||||||
return res.status(500).json({ error: 'Unsplash API key not configured' });
|
return res.status(500).json({ error: 'Pexels API key not configured' });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Stock Photos] Importing photo:', photoId);
|
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
|
// Download the image
|
||||||
const imageResponse = await fetch(downloadUrl);
|
const imageResponse = await fetch(downloadUrl);
|
||||||
|
|
||||||
@ -93,7 +106,7 @@ router.post('/import', async (req, res) => {
|
|||||||
// Generate unique filename
|
// Generate unique filename
|
||||||
const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
const timestamp = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
||||||
const randomId = crypto.randomBytes(8).toString('hex');
|
const randomId = crypto.randomBytes(8).toString('hex');
|
||||||
const filename = `unsplash-${photoId}-${randomId}.${ext}`;
|
const filename = `pexels-${photoId}-${randomId}.${ext}`;
|
||||||
const key = `images/${timestamp}/${filename}`;
|
const key = `images/${timestamp}/${filename}`;
|
||||||
|
|
||||||
// Upload to S3
|
// Upload to S3
|
||||||
|
|||||||
@ -49,6 +49,9 @@ services:
|
|||||||
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
|
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
|
||||||
S3_SECRET_KEY: ${S3_SECRET_KEY}
|
S3_SECRET_KEY: ${S3_SECRET_KEY}
|
||||||
S3_ENDPOINT: ${S3_ENDPOINT}
|
S3_ENDPOINT: ${S3_ENDPOINT}
|
||||||
|
INFISICAL_TOKEN: ${INFISICAL_TOKEN}
|
||||||
|
INFISICAL_SITE_URL: ${INFISICAL_SITE_URL}
|
||||||
|
PEXELS_API_KEY: ${PEXELS_API_KEY}
|
||||||
depends_on:
|
depends_on:
|
||||||
mysql:
|
mysql:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user