mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-23 22:11:09 +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
|
* 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>
|
||||||
|
@ -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'>
|
<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
|
||||||
|
@ -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':
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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',
|
||||||
|
Loading…
Reference in New Issue
Block a user