Wired image upload for Twitter setting

refs https://github.com/TryGhost/Team/issues/3318

- adds new file service that allows managing image and file uploads
- wires file upload and save to twitter setting
- fixes issue with dynamic SVG icon loading not working randomly
This commit is contained in:
Rishabh 2023-06-02 09:55:26 +05:30
parent eaa16c2f75
commit 0f7fc2855e
5 changed files with 51 additions and 39 deletions

View File

@ -1,5 +1,4 @@
import React, {useCallback, useEffect} from 'react';
import {ChangeEvent, useState} from 'react';
import React, {ChangeEvent} from 'react';
export interface FileUploadProps {
id: string;
@ -14,23 +13,12 @@ export interface FileUploadProps {
}
const FileUpload: React.FC<FileUploadProps> = ({id, onUpload, children, style, ...props}) => {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const handleFileChange = (event: ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) {
setSelectedFile(event.target.files[0]);
}
};
const handleFileUpload = useCallback(() => {
const selectedFile = event.target.files?.[0];
if (selectedFile) {
onUpload?.(selectedFile);
}
}, [onUpload, selectedFile]);
useEffect(() => {
handleFileUpload();
}, [handleFileUpload]);
};
return (
<label htmlFor={id} style={style} {...props}>

View File

@ -1,4 +1,4 @@
import React, {useEffect, useRef, useState} from 'react';
import React, {useEffect, useState} from 'react';
interface UseDynamicSVGImportOptions {
onCompleted?: (
@ -12,8 +12,8 @@ function useDynamicSVGImport(
name: string,
options: UseDynamicSVGImportOptions = {}
) {
const ImportedIconRef = useRef<React.FC<React.SVGProps<SVGSVGElement>>>();
const [loading, setLoading] = useState(false);
const [SvgComponent, setSvgComponent] = useState<React.FC<React.SVGProps<SVGSVGElement>> | null | undefined>(null);
const [error, setError] = useState<Error>();
const {onCompleted, onError} = options;
@ -21,21 +21,22 @@ function useDynamicSVGImport(
setLoading(true);
const importIcon = async (): Promise<void> => {
try {
ImportedIconRef.current = (
const SvgIcon: React.FC<React.SVGProps<SVGSVGElement>> = (
await import(`../assets/icons/${name}.svg`)
).ReactComponent;
onCompleted?.(name, ImportedIconRef.current);
setSvgComponent(() => SvgIcon);
onCompleted?.(name, SvgIcon);
} catch (err: any) {
onError?.(err);
setError(err);
setError(() => err);
} finally {
setLoading(false);
setLoading(() => false);
}
};
importIcon();
}, [name, onCompleted, onError]);
return {error, loading, SvgIcon: ImportedIconRef.current};
return {error, loading, SvgComponent};
}
export type IconSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | number;
@ -62,8 +63,8 @@ interface IconProps {
* - all icons must have all it's children color value set `currentColor`
* - all strokes must be paths and _NOT_ outlined objects. Stroke width should be set to 1.5px
*/
const Icon: React.FC<IconProps> = ({name, size = 'md', color = 'black', className}) => {
const {SvgIcon} = useDynamicSVGImport(name);
const Icon: React.FC<IconProps> = ({name, size = 'md', color = 'black', className = ''}) => {
const {SvgComponent} = useDynamicSVGImport(name);
let styles = '';
@ -81,7 +82,7 @@ const Icon: React.FC<IconProps> = ({name, size = 'md', color = 'black', classNam
case 'xl':
styles = 'w-10 h-10';
break;
default:
styles = 'w-5 h-5';
break;
@ -92,9 +93,9 @@ const Icon: React.FC<IconProps> = ({name, size = 'md', color = 'black', classNam
styles += ` text-${color}`;
}
if (SvgIcon) {
if (SvgComponent) {
return (
<SvgIcon className={`${styles} ${className}`} />
<SvgComponent className={`${styles} ${className}`} />
);
}
return null;

View File

@ -13,11 +13,11 @@ interface ImageUploadProps {
}
const ImageUpload: React.FC<ImageUploadProps> = ({
id,
label,
width,
height = '120px',
imageURL,
id,
label,
width,
height = '120px',
imageURL,
onUpload,
onDelete
}) => {

View File

@ -2,8 +2,12 @@ import React from 'react';
import setupGhostApi from '../../utils/api';
import {createContext} from 'react';
export interface FileService {
uploadImage: (file: File) => Promise<string>;
}
interface ServicesContextProps {
api: ReturnType<typeof setupGhostApi>;
fileService: FileService|null
}
interface ServicesProviderProps {
@ -12,14 +16,22 @@ interface ServicesProviderProps {
}
const ServicesContext = createContext<ServicesContextProps>({
api: setupGhostApi({ghostVersion: ''})
api: setupGhostApi({ghostVersion: ''}),
fileService: null
});
const ServicesProvider: React.FC<ServicesProviderProps> = ({children, ghostVersion}) => {
const apiService = setupGhostApi({ghostVersion});
const fileService = {
uploadImage: async (file: File): Promise<string> => {
const response = await apiService.images.upload({file});
return response.images[0].url;
}
};
return (
<ServicesContext.Provider value={{
api: apiService
api: apiService,
fileService
}}>
{children}
</ServicesContext.Provider>

View File

@ -1,9 +1,10 @@
import ImageUpload from '../../../admin-x-ds/global/ImageUpload';
import React from 'react';
import React, {useContext} from 'react';
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
import TextField from '../../../admin-x-ds/global/TextField';
import useSettingGroup from '../../../hooks/useSettingGroup';
import {FileService, ServicesContext} from '../../providers/ServiceProvider';
const Twitter: React.FC = () => {
const {
@ -17,7 +18,11 @@ const Twitter: React.FC = () => {
handleStateChange
} = useSettingGroup();
const [twitterTitle, twitterDescription, siteTitle, siteDescription] = getSettingValues(['twitter_title', 'twitter_description', 'title', 'description']) as string[];
const {fileService} = useContext(ServicesContext) as {fileService: FileService};
const [
twitterTitle, twitterDescription, twitterImage, siteTitle, siteDescription
] = getSettingValues(['twitter_title', 'twitter_description', 'twitter_image', 'title', 'description']) as string[];
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
updateSetting('twitter_title', e.target.value);
@ -27,12 +32,17 @@ const Twitter: React.FC = () => {
updateSetting('twitter_description', e.target.value);
};
const handleImageUpload = (file: File) => {
alert(file.name);
const handleImageUpload = async (file: File) => {
try {
const imageUrl = await fileService.uploadImage(file);
updateSetting('twitter_image', imageUrl);
} catch (err: any) {
// handle error
}
};
const handleImageDelete = () => {
alert('Delete twitter image');
updateSetting('twitter_image', '');
};
const values = (
@ -44,6 +54,7 @@ const Twitter: React.FC = () => {
<ImageUpload
height='200px'
id='twitter-image'
imageURL={twitterImage}
label='Upload twitter image'
onDelete={handleImageDelete}
onUpload={handleImageUpload}