Added global image upload component (AdminX)

refs. https://github.com/TryGhost/Team/issues/3318
This commit is contained in:
Peter Zimon 2023-06-01 18:54:11 +02:00
parent 9a1b78ae4f
commit bfcbb2b201
9 changed files with 129 additions and 8 deletions

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M18.0576 22.3846H5.94219C5.48317 22.3846 5.04294 22.2023 4.71836 21.8777C4.39377 21.5531 4.21143 21.1129 4.21143 20.6538V5.07692H19.7883V20.6538C19.7883 21.1129 19.606 21.5531 19.2814 21.8777C18.9568 22.2023 18.5166 22.3846 18.0576 22.3846Z"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M9.40381 17.1923V10.2692"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M14.5962 17.1923V10.2692"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M0.75 5.07692H23.25"></path><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" d="M14.5962 1.61539H9.40386C8.94484 1.61539 8.50461 1.79774 8.18003 2.12232C7.85544 2.4469 7.6731 2.88713 7.6731 3.34616V5.07693H16.3269V3.34616C16.3269 2.88713 16.1446 2.4469 15.82 2.12232C15.4954 1.79774 15.0552 1.61539 14.5962 1.61539Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -8,10 +8,12 @@ export interface FileUploadProps {
* Can be any component that has no default onClick eventh handline. E.g. buttons and links won't work * Can be any component that has no default onClick eventh handline. E.g. buttons and links won't work
*/ */
children?: React.ReactNode; children?: React.ReactNode;
className?: string;
onUpload: (file: File) => void; onUpload: (file: File) => void;
style: {}
} }
const FileUpload: React.FC<FileUploadProps> = ({id, onUpload, children, ...props}) => { const FileUpload: React.FC<FileUploadProps> = ({id, onUpload, children, style, ...props}) => {
const [selectedFile, setSelectedFile] = useState<File | null>(null); const [selectedFile, setSelectedFile] = useState<File | null>(null);
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => { const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
@ -31,7 +33,7 @@ const FileUpload: React.FC<FileUploadProps> = ({id, onUpload, children, ...props
}, [handleFileUpload]); }, [handleFileUpload]);
return ( return (
<label htmlFor={id} {...props}> <label htmlFor={id} style={style} {...props}>
<input id={id} type="file" hidden onChange={handleFileChange} /> <input id={id} type="file" hidden onChange={handleFileChange} />
{children} {children}
</label> </label>

View File

@ -0,0 +1,51 @@
import type {Meta, StoryObj} from '@storybook/react';
import ImageUpload from './ImageUpload';
const meta = {
title: 'Global / Image upload',
component: ImageUpload,
tags: ['autodocs'],
decorators: [(_story: any) => (<div style={{maxWidth: '600px'}}>{_story()}</div>)]
} satisfies Meta<typeof ImageUpload>;
export default meta;
type Story = StoryObj<typeof ImageUpload>;
export const Default: Story = {
args: {
id: 'image-upload-test',
label: 'Upload image',
onUpload: (file: File) => {
alert(`You're uploading: ${file.name}`);
}
}
};
export const Resized: Story = {
args: {
id: 'image-upload-test',
label: 'Upload image',
width: '480px',
height: '320px',
onUpload: (file: File) => {
alert(`You're uploading: ${file.name}`);
}
}
};
export const ImageUploaded: Story = {
args: {
id: 'image-upload-test',
label: 'Upload image',
width: '480px',
height: '320px',
imageURL: 'https://images.unsplash.com/photo-1685374156924-5230519f4ab3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wxMTc3M3wwfDF8YWxsfDI1fHx8fHx8Mnx8MTY4NTYzNzE3M3w&ixlib=rb-4.0.3&q=80&w=2000',
onUpload: (file: File) => {
alert(`You're uploading: ${file.name}`);
},
onDelete: () => {
alert('Delete image');
}
}
};

View File

@ -0,0 +1,50 @@
import FileUpload from './FileUpload';
import Icon from './Icon';
import React from 'react';
interface ImageUploadProps {
id: string;
label: React.ReactNode;
width?: string;
height?: string;
imageURL?: string;
onUpload: (file: File) => void;
onDelete: () => void;
}
const ImageUpload: React.FC<ImageUploadProps> = ({
id,
label,
width,
height = '120px',
imageURL,
onUpload,
onDelete
}) => {
if (imageURL) {
return (
<div className='group relative bg-cover' style={{
width: width,
height: height,
backgroundImage: `url(${imageURL})`
}}>
<button className='absolute right-4 top-4 hidden h-8 w-8 cursor-pointer items-center justify-center rounded bg-[rgba(0,0,0,0.75)] text-white hover:bg-black group-hover:flex' type='button' onClick={onDelete}>
<Icon color='white' name='trash' size='sm' />
</button>
</div>
);
} else {
return (
<FileUpload className={`flex cursor-pointer items-center justify-center rounded border border-grey-100 bg-grey-75 p-3 text-sm font-semibold text-grey-800 hover:text-black`} id={id} style={
{
width: width,
height: height
}
} onUpload={onUpload}>
{label}
</FileUpload>
);
}
};
export default ImageUpload;

View File

@ -38,7 +38,7 @@ const Select: React.FC<SelectProps> = ({title, prompt, options, onSelect, error,
<div className='flex flex-col'> <div className='flex flex-col'>
{title && <Heading useLabelTag={true}>{title}</Heading>} {title && <Heading useLabelTag={true}>{title}</Heading>}
<div className={`relative w-full after:pointer-events-none after:absolute ${clearBg ? 'after:right-0' : 'after:right-4'} after:block after:h-2 after:w-2 after:rotate-45 after:border-[1px] after:border-l-0 after:border-t-0 after:border-grey-900 after:content-[''] ${title ? 'after:top-[22px]' : 'after:top-[14px]'}`}> <div className={`relative w-full after:pointer-events-none after:absolute ${clearBg ? 'after:right-0' : 'after:right-4'} after:block after:h-2 after:w-2 after:rotate-45 after:border-[1px] after:border-l-0 after:border-t-0 after:border-grey-900 after:content-[''] ${title ? 'after:top-[22px]' : 'after:top-[14px]'}`}>
<select className={`w-full cursor-pointer appearance-none border-b ${!clearBg && 'bg-grey-100 px-[10px]'} py-2 outline-none ${error ? `border-red` : `border-grey-300 hover:border-grey-400 focus:border-grey-600`} ${title && `mt-2`}`} value={selectedOption} onChange={handleOptionChange}> <select className={`w-full cursor-pointer appearance-none border-b ${!clearBg && 'bg-grey-75 px-[10px]'} py-2 outline-none ${error ? `border-red` : `border-grey-500 hover:border-grey-700 focus:border-black`} ${title && `mt-2`}`} value={selectedOption} onChange={handleOptionChange}>
{prompt && <option value="">{prompt}</option>} {prompt && <option value="">{prompt}</option>}
{options.map(option => ( {options.map(option => (
<option <option

View File

@ -20,7 +20,7 @@ interface TextAreaProps {
} }
const TextArea: React.FC<TextAreaProps> = ({inputRef, title, value, rows = 3, maxLength, resize = 'none', error, placeholder, hint, clearBg = false, onChange, ...props}) => { const TextArea: React.FC<TextAreaProps> = ({inputRef, title, value, rows = 3, maxLength, resize = 'none', error, placeholder, hint, clearBg = false, onChange, ...props}) => {
let styles = `border-b ${!clearBg && 'bg-grey-100 px-[10px]'} py-2 ${error ? `border-red` : `border-grey-300 hover:border-grey-400 focus:border-grey-600`} ${title && `mt-2`}`; let styles = `border-b ${!clearBg && 'bg-grey-75 px-[10px]'} py-2 ${error ? `border-red` : `border-grey-500 hover:border-grey-700 focus:border-black`} ${title && `mt-2`}`;
switch (resize) { switch (resize) {
case 'both': case 'both':

View File

@ -40,7 +40,7 @@ const TextField: React.FC<TextFieldProps> = ({
{title && <Heading useLabelTag={true}>{title}</Heading>} {title && <Heading useLabelTag={true}>{title}</Heading>}
<input <input
ref={inputRef} ref={inputRef}
className={`border-b ${!clearBg && 'bg-grey-100 px-[10px]'} py-2 ${error ? `border-red` : `border-grey-300 hover:border-grey-400 focus:border-black`} ${(title && !clearBg) && `mt-2`} ${className}`} className={`border-b ${!clearBg && 'bg-grey-75 px-[10px]'} py-2 ${error ? `border-red` : `border-grey-500 hover:border-grey-700 focus:border-black`} ${(title && !clearBg) && `mt-2`} ${className}`}
defaultValue={value} defaultValue={value}
maxLength={maxLength} maxLength={maxLength}
placeholder={placeholder} placeholder={placeholder}

View File

@ -1,3 +1,4 @@
import ImageUpload from '../../../admin-x-ds/global/ImageUpload';
import React from 'react'; import React from 'react';
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup'; import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent'; import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
@ -26,12 +27,27 @@ const Twitter: React.FC = () => {
updateSetting('twitter_description', e.target.value); updateSetting('twitter_description', e.target.value);
}; };
const handleImageUpload = (file: File) => {
alert(file.name);
};
const handleImageDelete = () => {
alert('Delete twitter iamge');
};
const values = ( const values = (
<></> <></>
); );
const inputFields = ( const inputFields = (
<SettingGroupContent> <SettingGroupContent>
<ImageUpload
height='200px'
id='twitter-image'
label='Upload twitter image'
onDelete={handleImageDelete}
onUpload={handleImageUpload}
/>
<TextField <TextField
inputRef={focusRef} inputRef={focusRef}
placeholder={siteTitle} placeholder={siteTitle}

View File

@ -20,6 +20,7 @@ module.exports = {
grey: { grey: {
DEFAULT: '#ABB4BE', DEFAULT: '#ABB4BE',
50: '#FAFAFB', 50: '#FAFAFB',
75: '#F9FAFB',
100: '#F4F5F6', 100: '#F4F5F6',
200: '#EBEEF0', 200: '#EBEEF0',
300: '#DDE1E5', 300: '#DDE1E5',