docbrown/app/javascript/article-form/components/ArticleCoverImage.jsx
Ben Halpern ec7adf56fe
Allow admins to set cover image height configs (#19936)
* Initial basic work

* Bulk of related work, including find/replace on the inputs

* Adjust some tests

* Adjust some specs

* Fix a few more tests

* Clean up tests

* Adjust tests

* Test fiddle

* Adjust crop back to be a param

* Update tests

* Set proper defaults

* Fix some styling

* Adjust enrichment logic and tests

* Adjust form JS

* Update test snapshot

* Clean up formatting

* Fix spec name

* Adjust some css and defaults

* Adjust translation for image provider options

* Switch from fill to fill-down

* Proper fallback image

* Fix tests

* Update app/services/images/optimizer.rb

Co-authored-by: Mac Siri <mac@forem.com>

---------

Co-authored-by: Mac Siri <mac@forem.com>
2023-08-21 13:25:16 -04:00

239 lines
6.7 KiB
JavaScript

import { h, Fragment } from 'preact';
import { useState } from 'preact/hooks';
import PropTypes from 'prop-types';
import { addSnackbarItem } from '../../Snackbar';
import { generateMainImage } from '../actions';
import { validateFileInputs } from '../../packs/validateFileInputs';
import { onDragOver, onDragExit } from './dragAndDropHelpers';
import { Button } from '@crayons';
import { Spinner } from '@crayons/Spinner/Spinner';
import { DragAndDropZone } from '@utilities/dragAndDrop';
import { isNativeIOS } from '@utilities/runtime';
const NativeIosImageUpload = ({
extraProps,
uploadLabel,
isUploadingImage,
}) => (
<Fragment>
{isUploadingImage ? null : (
<Button
variant="outlined"
className="mr-2 whitespace-nowrap"
{...extraProps}
>
{uploadLabel}
</Button>
)}
</Fragment>
);
const StandardImageUpload = ({
uploadLabel,
handleImageUpload,
isUploadingImage,
coverImageHeight,
coverImageCrop,
}) =>
isUploadingImage ? null : (
<Fragment>
<label className="cursor-pointer crayons-btn crayons-btn--outlined crayons-tooltip__activator">
{uploadLabel}
<input
data-testid="cover-image-input"
id="cover-image-input"
type="file"
onChange={handleImageUpload}
accept="image/*"
className="screen-reader-only"
data-max-file-size-mb="25"
/>
<span data-testid="tooltip" className="crayons-tooltip__content" style={{minWidth:'190px'}}>
{coverImageCrop === 'crop' ? `Use a ratio of 1000:${coverImageHeight} ` : 'Minimum 1000px wide '}
for best results.
</span>
</label>
</Fragment>
);
export const ArticleCoverImage = ({ onMainImageUrlChange, mainImage, coverImageHeight, coverImageCrop }) => {
const [uploadError, setUploadError] = useState(false);
const [uploadErrorMessage, setUploadErrorMessage] = useState(null);
const [uploadingImage, setUploadingImage] = useState(false);
const onImageUploadSuccess = (...args) => {
onMainImageUrlChange(...args);
setUploadingImage(false);
};
const handleMainImageUpload = (event) => {
event.preventDefault();
setUploadingImage(true);
clearUploadError();
if (validateFileInputs()) {
const { files: image } = event.dataTransfer || event.target;
const payload = { image };
generateMainImage({
payload,
successCb: onImageUploadSuccess,
failureCb: onUploadError,
});
} else {
setUploadingImage(false);
}
};
const clearUploadError = () => {
setUploadError(false);
setUploadErrorMessage(null);
};
const onUploadError = (error) => {
setUploadingImage(false);
setUploadError(true);
setUploadErrorMessage(error.message);
};
const useNativeUpload = () => {
return isNativeIOS('imageUpload');
};
const initNativeImagePicker = (e) => {
e.preventDefault();
let options = { action: 'coverImageUpload' };
if (coverImageCrop === 'crop') {
options = { ...options, ratio: `1000.0 / ${coverImageHeight}.0`, };
}
window.ForemMobile?.injectNativeMessage('coverUpload', options);
};
const handleNativeMessage = (e) => {
const message = JSON.parse(e.detail);
if (message.namespace !== 'coverUpload') {
return;
}
/* eslint-disable no-case-declarations */
switch (message.action) {
case 'uploading':
setUploadingImage(true);
clearUploadError();
break;
case 'error':
setUploadingImage(false);
setUploadError(true);
setUploadErrorMessage(message.error);
break;
case 'success':
onMainImageUrlChange({
links: [message.link],
});
setUploadingImage(false);
break;
}
/* eslint-enable no-case-declarations */
};
const triggerMainImageRemoval = (e) => {
e.preventDefault();
onMainImageUrlChange({
links: [null],
});
};
const onDropImage = (event) => {
onDragExit(event);
if (event.dataTransfer.files.length > 1) {
addSnackbarItem({
message: 'Only one image can be dropped at a time.',
addCloseButton: true,
});
return;
}
handleMainImageUpload(event);
};
const uploadLabel = mainImage ? 'Change' : 'Add a cover image';
// When the component is rendered in an environment that supports a native
// image picker for image upload we want to add the aria-label attr and the
// onClick event to the UI button. This event will kick off the native UX.
// The props are unwrapped (using spread operator) in the button below
const extraProps = useNativeUpload()
? {
onClick: initNativeImagePicker,
'aria-label': 'Upload cover image',
}
: {};
// Native Bridge messages come through ForemMobile events
document.addEventListener('ForemMobile', handleNativeMessage);
return (
<DragAndDropZone
onDragOver={onDragOver}
onDragExit={onDragExit}
onDrop={onDropImage}
>
<div className="crayons-article-form__cover" role="presentation">
{!uploadingImage && mainImage && (
<img
src={mainImage}
className="crayons-article-form__cover__image"
width="250"
height="105"
alt="Post cover"
/>
)}
<div className="flex items-center">
{uploadingImage && (
<span class="lh-base pl-1 border-0 py-2 inline-block">
<Spinner /> Uploading...
</span>
)}
<Fragment>
{useNativeUpload() ? (
<NativeIosImageUpload
isUploadingImage={uploadingImage}
extraProps={extraProps}
uploadLabel={uploadLabel}
/>
) : (
<StandardImageUpload
isUploadingImage={uploadingImage}
uploadLabel={uploadLabel}
coverImageHeight={coverImageHeight}
coverImageCrop={coverImageCrop}
handleImageUpload={handleMainImageUpload}
/>
)}
{mainImage && !uploadingImage && (
<Button variant="ghost-danger" onClick={triggerMainImageRemoval}>
Remove
</Button>
)}
</Fragment>
</div>
{uploadError && (
<p className="articleform__uploaderror">{uploadErrorMessage}</p>
)}
</div>
</DragAndDropZone>
);
};
ArticleCoverImage.propTypes = {
mainImage: PropTypes.string,
onMainImageUrlChange: PropTypes.func.isRequired,
coverImageHeight: PropTypes.string.isRequired,
coverImageCrop: PropTypes.string.isRequired,
};
ArticleCoverImage.displayName = 'ArticleCoverImage';