From 2fdc2b3497a8ae69207b7cf1f993a1e609bc0e76 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Wed, 4 Mar 2026 23:10:55 +0000 Subject: [PATCH] Oo --- backend/src/index.js | 6 +- backend/src/routes/pexels.js | 87 ++++++++++++++++++++-------- frontend/src/helpers/pexels.ts | 102 ++++++++++++++++++++++----------- 3 files changed, 133 insertions(+), 62 deletions(-) diff --git a/backend/src/index.js b/backend/src/index.js index 5184627..8e418d3 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -157,7 +157,9 @@ if (fs.existsSync(publicDir)) { }); } -const PORT = process.env.NODE_ENV === 'dev_stage' ? 3000 : 8080; +const PORT = Number( + process.env.PORT || (process.env.NODE_ENV === 'dev_stage' ? 3000 : 8080), +); db.sequelize.sync().then(function () { app.listen(PORT, () => { @@ -165,4 +167,4 @@ db.sequelize.sync().then(function () { }); }); -module.exports = app; \ No newline at end of file +module.exports = app; diff --git a/backend/src/routes/pexels.js b/backend/src/routes/pexels.js index 8298595..59b5e1c 100644 --- a/backend/src/routes/pexels.js +++ b/backend/src/routes/pexels.js @@ -4,6 +4,47 @@ const { pexelsKey, pexelsQuery } = require('../config'); const fetch = require('node-fetch'); const KEY = pexelsKey; +const REQUEST_TIMEOUT_MS = 4500; + +const FALLBACK_IMAGE = { + src: { + original: + 'https://images.pexels.com/photos/8199252/pexels-photo-8199252.jpeg', + }, + photographer: 'Yan Krukau', + photographer_url: 'https://www.pexels.com/@yankrukov', +}; + +const FALLBACK_VIDEO = { + video_files: [], + user: { + name: 'Pexels', + url: 'https://www.pexels.com/', + }, +}; + +async function fetchJsonWithTimeout(url, headers) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); + + try { + const response = await fetch(url, { headers, signal: controller.signal }); + if (!response.ok) { + return null; + } + + const contentType = response.headers.get('content-type') || ''; + if (!contentType.includes('application/json')) { + return null; + } + + return await response.json(); + } catch (error) { + return null; + } finally { + clearTimeout(timeoutId); + } +} router.get('/image', async (req, res) => { const headers = { @@ -14,13 +55,10 @@ router.get('/image', async (req, res) => { const perPage = 1; const url = `https://api.pexels.com/v1/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`; - try { - const response = await fetch(url, { headers }); - const data = await response.json(); - res.status(200).json(data.photos[0]); - } catch (error) { - res.status(200).json({ error: 'Failed to fetch image' }); - } + const data = await fetchJsonWithTimeout(url, headers); + const image = data && Array.isArray(data.photos) ? data.photos[0] : null; + + res.status(200).json(image || FALLBACK_IMAGE); }); router.get('/video', async (req, res) => { @@ -32,13 +70,10 @@ router.get('/video', async (req, res) => { const perPage = 1; const url = `https://api.pexels.com/videos/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`; - try { - const response = await fetch(url, { headers }); - const data = await response.json(); - res.status(200).json(data.videos[0]); - } catch (error) { - res.status(200).json({ error: 'Failed to fetch video' }); - } + const data = await fetchJsonWithTimeout(url, headers); + const video = data && Array.isArray(data.videos) ? data.videos[0] : null; + + res.status(200).json(video || FALLBACK_VIDEO); }); router.get('/multiple-images', async (req, res) => { @@ -52,11 +87,6 @@ router.get('/multiple-images', async (req, res) => { const orientation = 'square'; const perPage = 1; - const fallbackImage = { - src: 'https://images.pexels.com/photos/8199252/pexels-photo-8199252.jpeg', - photographer: 'Yan Krukau', - photographer_url: 'https://www.pexels.com/@yankrukov', - }; const fetchFallbackImage = async () => { try { const response = await fetch('https://picsum.photos/600'); @@ -66,13 +96,19 @@ router.get('/multiple-images', async (req, res) => { photographer_url: 'https://picsum.photos/', }; } catch (error) { - return fallbackImage; + return { + src: FALLBACK_IMAGE.src.original, + photographer: FALLBACK_IMAGE.photographer, + photographer_url: FALLBACK_IMAGE.photographer_url, + }; } }; const fetchImage = async (query) => { const url = `https://api.pexels.com/v1/search?query=${query}&orientation=${orientation}&per_page=${perPage}&page=1`; - const response = await fetch(url, { headers }); - const data = await response.json(); + const data = await fetchJsonWithTimeout(url, headers); + if (!data || !Array.isArray(data.photos)) { + return null; + } return data.photos[0] || null; }; @@ -83,9 +119,10 @@ router.get('/multiple-images', async (req, res) => { if (result.status === 'fulfilled' && result.value) { const image = result.value; return { - src: image.src?.original || fallbackImage.src, - photographer: image.photographer || fallbackImage.photographer, - photographer_url: image.photographer_url || fallbackImage.photographer_url, + src: image.src?.original || FALLBACK_IMAGE.src.original, + photographer: image.photographer || FALLBACK_IMAGE.photographer, + photographer_url: + image.photographer_url || FALLBACK_IMAGE.photographer_url, }; } else { const fallback = await fetchFallbackImage(); diff --git a/frontend/src/helpers/pexels.ts b/frontend/src/helpers/pexels.ts index dac222c..3531780 100644 --- a/frontend/src/helpers/pexels.ts +++ b/frontend/src/helpers/pexels.ts @@ -1,23 +1,51 @@ import axios from 'axios'; -export async function getPexelsImage() { +const FALLBACK_IMAGE = { + src: { + original: + 'https://images.pexels.com/photos/8199252/pexels-photo-8199252.jpeg', + }, + photographer: 'Yan Krukau', + photographer_url: 'https://www.pexels.com/@yankrukov', +}; + +const FALLBACK_VIDEO = { + video_files: [], + user: { + name: 'Pexels', + url: 'https://www.pexels.com/', + }, +}; + +async function safeGet(url: string, params?: Record) { try { - const response = await axios.get('pexels/image'); - return response.data; + const response = await axios.get(url, { + params, + timeout: 7000, + // Keep UI stable even if upstream/proxy returns 5xx. + validateStatus: () => true, + }); + + if (response.status >= 200 && response.status < 300) { + return response.data; + } + + console.warn(`[pexels] ${url} returned ${response.status}`); + return null; } catch (error) { - console.error('Error fetching image:', error); + console.warn(`[pexels] ${url} unavailable`); return null; } } +export async function getPexelsImage() { + const data = await safeGet('pexels/image'); + return data || FALLBACK_IMAGE; +} + export async function getPexelsVideo() { - try { - const response = await axios.get('pexels/video'); - return response.data; - } catch (error) { - console.error('Error fetching video:', error); - return null; - } + const data = await safeGet('pexels/video'); + return data || FALLBACK_VIDEO; } @@ -29,48 +57,52 @@ export async function getMultiplePexelsImages( const normalizeQuery = (query) => query.trim().toLowerCase().replace(/\s+/g, ''); + if (typeof window === 'undefined') { + return queries.map(() => FALLBACK_IMAGE); + } + while (localStorageLock) { - await new Promise((resolve) => setTimeout(resolve, 50)); + await new Promise((resolve) => setTimeout(resolve, 50)); } localStorageLock = true; - const cachedImages = JSON.parse(localStorage.getItem('pexelsImagesCache')) || {}; + try { + const cachedImages = + JSON.parse(localStorage.getItem('pexelsImagesCache')) || {}; + const isImageCached = (query) => { + const normalizedQuery = normalizeQuery(query); + const cached = cachedImages[normalizedQuery]; + const isCached = + cached && cached.src && cached.photographer && cached.photographer_url; + return isCached; + }; - const isImageCached = (query) => { - const normalizedQuery = normalizeQuery(query); - const cached = cachedImages[normalizedQuery]; - const isCached = cached && cached.src && cached.photographer && cached.photographer_url; - return isCached; - }; + const missingQueries = queries.filter((query) => !isImageCached(query)); - const missingQueries = queries.filter((query) => !isImageCached(query)); + if (missingQueries.length > 0) { + const queryString = missingQueries.join(','); - if (missingQueries.length > 0) { - const queryString = missingQueries.join(','); - - try { - const response = await axios.get('pexels/multiple-images', { - params: { queries: queryString }, + const response = await safeGet('pexels/multiple-images', { + queries: queryString, }); missingQueries.forEach((query, index) => { const normalizedQuery = normalizeQuery(query); if (!cachedImages[normalizedQuery]) { - cachedImages[normalizedQuery] = response.data[index]; + cachedImages[normalizedQuery] = response?.[index] || FALLBACK_IMAGE; } }); localStorage.setItem('pexelsImagesCache', JSON.stringify(cachedImages)); - - } catch (error) { - console.error(error); } - } - const result = queries.map((query) => cachedImages[normalizeQuery(query)]); + const result = queries.map( + (query) => cachedImages[normalizeQuery(query)] || FALLBACK_IMAGE, + ); - localStorageLock = false; - - return result; + return result; + } finally { + localStorageLock = false; + } }