feat: add cover image upload functionality with loading state in article creation form
This commit is contained in:
@@ -54,6 +54,7 @@ function validateContentFile(file: File): string | null {
|
|||||||
|
|
||||||
export const CreateArticleForm = () => {
|
export const CreateArticleForm = () => {
|
||||||
const [coverImageFile, setCoverImageFile] = useState<File | null>(null);
|
const [coverImageFile, setCoverImageFile] = useState<File | null>(null);
|
||||||
|
const [coverImageUploading, setCoverImageUploading] = useState(false);
|
||||||
const [contentFile, setContentFile] = useState<File | null>(null);
|
const [contentFile, setContentFile] = useState<File | null>(null);
|
||||||
const coverImageUrlRef = useRef<string | null>(null);
|
const coverImageUrlRef = useRef<string | null>(null);
|
||||||
|
|
||||||
@@ -78,10 +79,8 @@ export const CreateArticleForm = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const title = useWatch({
|
const title = useWatch({ control: form.control, name: 'title' });
|
||||||
control: form.control,
|
const coverImageUrl = useWatch({ control: form.control, name: 'coverImageUrl' });
|
||||||
name: 'title',
|
|
||||||
});
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!title) return;
|
if (!title) return;
|
||||||
form.setValue('slug', slugify(title).toLowerCase());
|
form.setValue('slug', slugify(title).toLowerCase());
|
||||||
@@ -93,6 +92,7 @@ export const CreateArticleForm = () => {
|
|||||||
coverImageUrlRef.current = null;
|
coverImageUrlRef.current = null;
|
||||||
}
|
}
|
||||||
setCoverImageFile(null);
|
setCoverImageFile(null);
|
||||||
|
setCoverImageUploading(false);
|
||||||
setContentFile(null);
|
setContentFile(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -128,10 +128,13 @@ export const CreateArticleForm = () => {
|
|||||||
setCoverImageFile(file);
|
setCoverImageFile(file);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
setCoverImageFile(null);
|
setCoverImageFile(null);
|
||||||
|
setCoverImageUploading(false);
|
||||||
form.setValue('coverImageUrl', '');
|
form.setValue('coverImageUrl', '');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setCoverImageUploading(true);
|
||||||
const fileMetadataResult = await uploadFile(file);
|
const fileMetadataResult = await uploadFile(file);
|
||||||
|
setCoverImageUploading(false);
|
||||||
if (!fileMetadataResult.ok) {
|
if (!fileMetadataResult.ok) {
|
||||||
setCoverImageFile(null);
|
setCoverImageFile(null);
|
||||||
form.setValue('coverImageUrl', '');
|
form.setValue('coverImageUrl', '');
|
||||||
@@ -255,6 +258,8 @@ export const CreateArticleForm = () => {
|
|||||||
label='Cover image'
|
label='Cover image'
|
||||||
description='PNG, JPG, GIF, WebP accepted'
|
description='PNG, JPG, GIF, WebP accepted'
|
||||||
error={form.formState.errors.coverImageUrl?.message}
|
error={form.formState.errors.coverImageUrl?.message}
|
||||||
|
previewUrl={coverImageUrl || undefined}
|
||||||
|
isUploading={coverImageUploading}
|
||||||
icon={
|
icon={
|
||||||
<Image
|
<Image
|
||||||
src={ImageLogo}
|
src={ImageLogo}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
FieldDescription,
|
FieldDescription,
|
||||||
FieldError,
|
FieldError,
|
||||||
} from '@/ui/components/shadcn/field';
|
} from '@/ui/components/shadcn/field';
|
||||||
|
import { Spinner } from '@/ui/components/shadcn/spinner';
|
||||||
import {
|
import {
|
||||||
FileUpload,
|
FileUpload,
|
||||||
FileUploadDropzone,
|
FileUploadDropzone,
|
||||||
@@ -29,6 +30,8 @@ export interface FileUploadFieldProps {
|
|||||||
description?: string;
|
description?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
icon?: React.ReactNode;
|
icon?: React.ReactNode;
|
||||||
|
previewUrl?: string;
|
||||||
|
isUploading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileUploadField: React.FC<FileUploadFieldProps> = ({
|
export const FileUploadField: React.FC<FileUploadFieldProps> = ({
|
||||||
@@ -41,6 +44,8 @@ export const FileUploadField: React.FC<FileUploadFieldProps> = ({
|
|||||||
description,
|
description,
|
||||||
error,
|
error,
|
||||||
icon,
|
icon,
|
||||||
|
previewUrl,
|
||||||
|
isUploading,
|
||||||
}) => {
|
}) => {
|
||||||
const handleAccept = useCallback(
|
const handleAccept = useCallback(
|
||||||
(files: File[]) => {
|
(files: File[]) => {
|
||||||
@@ -90,7 +95,24 @@ export const FileUploadField: React.FC<FileUploadFieldProps> = ({
|
|||||||
<FileUploadList>
|
<FileUploadList>
|
||||||
{file && (
|
{file && (
|
||||||
<FileUploadItem value={file}>
|
<FileUploadItem value={file}>
|
||||||
<FileUploadItemPreview />
|
<FileUploadItemPreview
|
||||||
|
render={
|
||||||
|
isUploading
|
||||||
|
? () => (
|
||||||
|
<Spinner className='size-5 text-muted-foreground' />
|
||||||
|
)
|
||||||
|
: previewUrl
|
||||||
|
? () => (
|
||||||
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
|
<img
|
||||||
|
src={previewUrl}
|
||||||
|
alt='Uploaded image'
|
||||||
|
className='size-full object-cover'
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
<FileUploadItemMetadata size='sm' />
|
<FileUploadItemMetadata size='sm' />
|
||||||
<FileUploadItemDelete asChild>
|
<FileUploadItemDelete asChild>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user