From 6f786e68eb3f24a3daac80e3d93ee0005ac6327a Mon Sep 17 00:00:00 2001 From: alexsparkes Date: Tue, 26 Nov 2024 17:24:49 +0000 Subject: [PATCH] refactor(photo-information): Split up into components --- src/features/background/api/utils.js | 34 ++ .../background/components/ActionButtons.jsx | 56 ++++ .../components/InformationItems.jsx | 57 ++++ .../components/PhotoInformation.jsx | 295 ++++-------------- .../background/components/UnsplashStats.jsx | 29 ++ 5 files changed, 243 insertions(+), 228 deletions(-) create mode 100644 src/features/background/api/utils.js create mode 100644 src/features/background/components/ActionButtons.jsx create mode 100644 src/features/background/components/InformationItems.jsx create mode 100644 src/features/background/components/UnsplashStats.jsx diff --git a/src/features/background/api/utils.js b/src/features/background/api/utils.js new file mode 100644 index 00000000..768bd940 --- /dev/null +++ b/src/features/background/api/utils.js @@ -0,0 +1,34 @@ +import variables from 'config/variables'; + +/** + * It takes a URL, fetches the resource, and returns a URL to the resource. + * @param {string} url The URL to fetch. + * @returns A promise that resolves to a blob URL. + */ +export const toDataURL = async (url) => { + const res = await fetch(url); + return URL.createObjectURL(await res.blob()); +}; + +/** + * It takes a string, makes it lowercase, removes commas, and replaces spaces with dashes. + * @param {string} text The string to format. + * @returns A function that takes a string and returns a string. + */ +export const formatText = (text) => { + return text.toLowerCase().replaceAll(',', '').replaceAll(' ', '-'); +}; + +/** + * It downloads an image from a URL and saves it to the user's computer. + * @param {object} info The photo information. + */ +export const downloadImage = async (info) => { + const link = document.createElement('a'); + link.href = await toDataURL(info.url); + link.download = `mue-${formatText(info.credit)}-${formatText(info.location)}.jpg`; // image is more likely to be webp or avif btw + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + variables.stats.postEvent('feature', 'background', 'download'); +}; diff --git a/src/features/background/components/ActionButtons.jsx b/src/features/background/components/ActionButtons.jsx new file mode 100644 index 00000000..8f0a428a --- /dev/null +++ b/src/features/background/components/ActionButtons.jsx @@ -0,0 +1,56 @@ +import { Tooltip } from 'components/Elements'; +import { + MdIosShare as Share, + MdGetApp as Download, + MdVisibilityOff as VisibilityOff, +} from 'react-icons/md'; +import Favourite from './Favourite'; +import { downloadImage } from '../api/utils'; +import variables from 'config/variables'; + +const ActionButtons = ({ + info, + favouriteTooltipText, + setFavouriteTooltipText, + openShareModal, + openExcludeModal, +}) => { + return ( +
+ {!info.offline && ( + + openShareModal(true)} /> + + )} + + setFavouriteTooltipText(text)} + /> + + {!info.offline && ( + + downloadImage(info)} /> + + )} + {info.pun && info.category && ( + + openExcludeModal(true)} /> + + )} +
+ ); +}; + +export default ActionButtons; diff --git a/src/features/background/components/InformationItems.jsx b/src/features/background/components/InformationItems.jsx new file mode 100644 index 00000000..5b3d86cd --- /dev/null +++ b/src/features/background/components/InformationItems.jsx @@ -0,0 +1,57 @@ +import { + MdLocationOn, + MdPhotoCamera, + MdCrop as Resolution, + MdCategory as Category, + MdSource as Source, +} from 'react-icons/md'; +import variables from 'config/variables'; + +const InformationItems = ({ info, width, height, api }) => { + return ( +
+ {info.location && info.location !== 'N/A' && ( +
+ + {info.location} +
+ )} + {info.camera && info.camera !== 'N/A' && ( +
+ + {info.camera} +
+ )} +
+ + + {width}x{height} + +
+ {info.category && ( +
+ + {info.category[0].toUpperCase() + info.category.slice(1)} +
+ )} + {api && ( +
+ + + {info.photoURL ? ( + + {api.charAt(0).toUpperCase() + api.slice(1)} + + ) : ( + + {api.charAt(0).toUpperCase() + api.slice(1)} + + )} + +
+ )} +
+ ); +}; + +export default InformationItems; diff --git a/src/features/background/components/PhotoInformation.jsx b/src/features/background/components/PhotoInformation.jsx index 0f5ff667..7e8b1951 100644 --- a/src/features/background/components/PhotoInformation.jsx +++ b/src/features/background/components/PhotoInformation.jsx @@ -1,65 +1,19 @@ import variables from 'config/variables'; -import { useState, memo } from 'react'; -import Favourite from './Favourite'; -import { - MdInfo, - MdLocationOn, - MdPhotoCamera, - MdCrop as Resolution, - MdGetApp as Download, - MdVisibility as Views, - MdIosShare as Share, - MdSource as Source, - MdFavorite as MdFavourite, - MdCategory as Category, - MdVisibilityOff as VisibilityOff, -} from 'react-icons/md'; -import { Tooltip } from 'components/Elements'; - +import { useState, memo, useEffect, useCallback } from 'react'; +import { MdInfo, MdLocationOn } from 'react-icons/md'; import Modal from 'react-modal'; import { ShareModal } from 'components/Elements'; import ExcludeModal from './ExcludeModal'; - -/** - * It takes a URL, fetches the resource, and returns a URL to the resource. - * @param {string} url The URL to fetch. - * @returns A promise that resolves to a blob URL. - */ -const toDataURL = async (url) => { - const res = await fetch(url); - return URL.createObjectURL(await res.blob()); -}; - -/** - * It takes a string, makes it lowercase, removes commas, and replaces spaces with dashes. - * @param {string} text The string to format. - * @returns A function that takes a string and returns a string. - */ -const formatText = (text) => { - return text.toLowerCase().replaceAll(',', '').replaceAll(' ', '-'); -}; - -/** - * It downloads an image from a URL and saves it to the user's computer. - * @param {object} info The photo information. - */ -const downloadImage = async (info) => { - const link = document.createElement('a'); - link.href = await toDataURL(info.url); - link.download = `mue-${formatText(info.credit)}-${formatText(info.location)}.jpg`; // image is more likely to be webp or avif btw - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - variables.stats.postEvent('feature', 'background', 'download'); -}; +import InformationItems from './InformationItems'; +import ActionButtons from './ActionButtons'; +import UnsplashStats from './UnsplashStats'; function PhotoInformation({ info, url, api }) { const [width, setWidth] = useState(0); const [height, setHeight] = useState(0); const [usePhotoMap, setPhotoMap] = useState(false); const [useMapIcon, setMapIcon] = useState(true); - const [showExtraInfo, setshowExtraInfo] = useState(false); - //const [showOld, setShowOld] = useState(true); + const [showExtraInfo, setShowExtraInfo] = useState(false); const [other, setOther] = useState(false); const [shareModal, openShareModal] = useState(false); const [excludeModal, openExcludeModal] = useState(false); @@ -67,6 +21,47 @@ function PhotoInformation({ info, url, api }) { variables.getMessage('widgets.quote.favourite'), ); + useEffect(() => { + const img = new Image(); + img.onload = (event) => { + setWidth(event.target.width); + setHeight(event.target.height); + }; + img.src = url; + }, [url]); + + const photoMap = useCallback(() => { + if ( + localStorage.getItem('photoMap') !== 'true' || + !info.latitude || + !info.longitude || + usePhotoMap === false + ) { + return null; + } + + const tile = `${variables.constants.API_URL}/map?latitude=${info.latitude}&longitude=${info.longitude}`; + return ( + + location + + ); + }, [info.latitude, info.longitude, usePhotoMap]); + + useEffect(() => { + const photoInformationElement = document.getElementsByClassName('photoInformation')[0]; + if (photoInformationElement) { + photoInformationElement.onmouseover = () => { + setPhotoMap(true); + setMapIcon(false); + }; + } + }, []); + if (info.hidden === true || !info.credit) { return null; } @@ -74,7 +69,6 @@ function PhotoInformation({ info, url, api }) { let credit = info.credit; let photo = variables.getMessage('widgets.background.credit'); - // unsplash credit if (info.photographerURL && info.photographerURL !== '' && !info.offline && api) { photo = ( @@ -82,23 +76,12 @@ function PhotoInformation({ info, url, api }) { ); credit = ( - <> - - {info.credit} - - + + {info.credit} + ); } - // get resolution - const img = new Image(); - img.onload = (event) => { - setWidth(event.target.width); - setHeight(event.target.height); - }; - img.src = url; - - // info is still there because we want the favourite button to work if (localStorage.getItem('photoInformation') === 'false') { return (
@@ -112,160 +95,11 @@ function PhotoInformation({ info, url, api }) { ); } - let showingPhotoMap = false; - const photoMap = () => { - if ( - localStorage.getItem('photoMap') !== 'true' || - !info.latitude || - !info.longitude || - usePhotoMap === false - ) { - return null; - } - - const tile = - variables.constants.API_URL + `/map?latitude=${info.latitude}&longitude=${info.longitude}`; - showingPhotoMap = true; - - return ( - - location - - ); - }; - - const InformationItems = () => { - return ( -
- {info.location && info.location !== 'N/A' ? ( -
- - {info.location} -
- ) : null} - {info.camera && info.camera !== 'N/A' ? ( -
- - {info.camera} -
- ) : null} -
- - - {width}x{height} - -
- {info.category && ( -
- - {info.category[0].toUpperCase() + info.category.slice(1)} -
- )} - {api && ( -
- - - {info.photoURL ? ( - - {api.charAt(0).toUpperCase() + api.slice(1)} - - ) : ( - - {api.charAt(0).toUpperCase() + api.slice(1)} - - )} - -
- )} -
- ); - }; - - const ActionButtons = () => { - return ( -
- {!info.offline && ( - - openShareModal(true)} /> - - )} - - setFavouriteTooltipText(text)} - /> - - {!info.offline && ( - - downloadImage(info)} /> - - )} - {info.pun && info.category && ( - - openExcludeModal(true)} /> - - )} -
- ); - }; - - const UnsplashStats = () => { - return ( -
-
- - {info.views.toLocaleString()} -
-
- - {info.downloads.toLocaleString()} -
- {!!info.likes ? ( -
- - {info.likes.toLocaleString()} -
- ) : null} -
- ); - }; - let photoMapClassList = 'map-concept'; if (photoMap() !== null) { photoMapClassList += ' photoMap'; } - // only request map image if the user looks at the photo information - // this is to reduce requests to the api - try { - document.getElementsByClassName('photoInformation')[0].onmouseover = () => { - try { - setPhotoMap(true); - setMapIcon(false); - } catch (e) {} - }; - } catch (e) {} - const widgetStyle = localStorage.getItem('widgetStyle'); return ( @@ -302,18 +136,18 @@ function PhotoInformation({ info, url, api }) {
)} - {widgetStyle !== 'legacy' || other ? ( + {(widgetStyle !== 'legacy' || other) && (
setshowExtraInfo(true)} - onMouseLeave={() => setshowExtraInfo(false)} + onMouseEnter={() => setShowExtraInfo(true)} + onMouseLeave={() => setShowExtraInfo(false)} >
{useMapIcon || photoMap() === null ? : ''} {photoMap()}
- {showingPhotoMap && ( + {photoMap() && (
{(showExtraInfo || other) && info.description ? info.description.length > 40 - ? info.description.substring(0, 40) + '...' + ? `${info.description.substring(0, 40)}...` : info.description : info.location?.split(',').slice(-2).join(', ').trim()} @@ -361,20 +195,25 @@ function PhotoInformation({ info, url, api }) { {photo} {credit}
- {info.views && info.downloads !== null ? : null} + {info.views && info.downloads !== null && }
- - {(showExtraInfo || other) && excludeModal === false ? ( + {(showExtraInfo || other) && !excludeModal && ( <> {variables.getMessage('widgets.background.information')} - - + + - ) : null} + )} - ) : null} + )} ); } diff --git a/src/features/background/components/UnsplashStats.jsx b/src/features/background/components/UnsplashStats.jsx new file mode 100644 index 00000000..e38eff9d --- /dev/null +++ b/src/features/background/components/UnsplashStats.jsx @@ -0,0 +1,29 @@ +import { + MdFavorite as MdFavourite, + MdGetApp as Download, + MdVisibility as Views, +} from 'react-icons/md'; +import variables from 'config/variables'; + +const UnsplashStats = ({ info }) => { + return ( +
+
+ + {info.views.toLocaleString()} +
+
+ + {info.downloads.toLocaleString()} +
+ {!!info.likes && ( +
+ + {info.likes.toLocaleString()} +
+ )} +
+ ); +}; + +export default UnsplashStats;