mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-27 10:42:45 +03:00
Added global image upload component (AdminX)
refs. https://github.com/TryGhost/Team/issues/3318
This commit is contained in:
parent
9a1b78ae4f
commit
bfcbb2b201
@ -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 |
@ -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
|
||||
*/
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
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 handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
@ -31,7 +33,7 @@ const FileUpload: React.FC<FileUploadProps> = ({id, onUpload, children, ...props
|
||||
}, [handleFileUpload]);
|
||||
|
||||
return (
|
||||
<label htmlFor={id} {...props}>
|
||||
<label htmlFor={id} style={style} {...props}>
|
||||
<input id={id} type="file" hidden onChange={handleFileChange} />
|
||||
{children}
|
||||
</label>
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
};
|
50
ghost/admin-x-settings/src/admin-x-ds/global/ImageUpload.tsx
Normal file
50
ghost/admin-x-settings/src/admin-x-ds/global/ImageUpload.tsx
Normal 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;
|
@ -38,7 +38,7 @@ const Select: React.FC<SelectProps> = ({title, prompt, options, onSelect, error,
|
||||
<div className='flex flex-col'>
|
||||
{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]'}`}>
|
||||
<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>}
|
||||
{options.map(option => (
|
||||
<option
|
||||
|
@ -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}) => {
|
||||
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) {
|
||||
case 'both':
|
||||
|
@ -40,7 +40,7 @@ const TextField: React.FC<TextFieldProps> = ({
|
||||
{title && <Heading useLabelTag={true}>{title}</Heading>}
|
||||
<input
|
||||
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}
|
||||
maxLength={maxLength}
|
||||
placeholder={placeholder}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import ImageUpload from '../../../admin-x-ds/global/ImageUpload';
|
||||
import React from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||
@ -26,12 +27,27 @@ const Twitter: React.FC = () => {
|
||||
updateSetting('twitter_description', e.target.value);
|
||||
};
|
||||
|
||||
const handleImageUpload = (file: File) => {
|
||||
alert(file.name);
|
||||
};
|
||||
|
||||
const handleImageDelete = () => {
|
||||
alert('Delete twitter iamge');
|
||||
};
|
||||
|
||||
const values = (
|
||||
<></>
|
||||
);
|
||||
|
||||
const inputFields = (
|
||||
<SettingGroupContent>
|
||||
<ImageUpload
|
||||
height='200px'
|
||||
id='twitter-image'
|
||||
label='Upload twitter image'
|
||||
onDelete={handleImageDelete}
|
||||
onUpload={handleImageUpload}
|
||||
/>
|
||||
<TextField
|
||||
inputRef={focusRef}
|
||||
placeholder={siteTitle}
|
||||
|
@ -20,6 +20,7 @@ module.exports = {
|
||||
grey: {
|
||||
DEFAULT: '#ABB4BE',
|
||||
50: '#FAFAFB',
|
||||
75: '#F9FAFB',
|
||||
100: '#F4F5F6',
|
||||
200: '#EBEEF0',
|
||||
300: '#DDE1E5',
|
||||
|
Loading…
Reference in New Issue
Block a user