-
Notifications
You must be signed in to change notification settings - Fork 178
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
154 additions
and
64 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
'use client'; | ||
|
||
import { useState } from 'react'; | ||
import { useDropzone } from 'react-dropzone'; | ||
import { toast } from 'sonner'; | ||
import { Loader2 } from 'lucide-react'; | ||
import { uploadSingleFile } from '@/lib/uploader'; | ||
|
||
interface ImageUploaderProps { | ||
value: string; | ||
onChange: (url: string) => void; | ||
showGeneratedImage?: boolean; | ||
} | ||
|
||
export function ImageUploader({ value, onChange }: ImageUploaderProps) { | ||
const [isUploading, setIsUploading] = useState(false); | ||
const [image, setImage] = useState<string | null>(value); | ||
|
||
const onDrop = async (acceptedFiles: File[]) => { | ||
const file = acceptedFiles[0]; | ||
if (!file) return; | ||
|
||
try { | ||
setIsUploading(true); | ||
const response = await uploadSingleFile(file); | ||
onChange(response.url); | ||
setImage(response.url); | ||
toast.success('Image uploaded successfully'); | ||
} catch (error) { | ||
console.error('Upload error:', error); | ||
toast.error('Failed to upload image'); | ||
} finally { | ||
setIsUploading(false); | ||
} | ||
}; | ||
|
||
const { getRootProps, getInputProps, isDragActive } = useDropzone({ | ||
onDrop, | ||
accept: { | ||
'image/*': ['.png', '.jpg', '.jpeg', '.gif'], | ||
}, | ||
maxFiles: 1, | ||
}); | ||
return ( | ||
<div className="space-y-4"> | ||
<div | ||
{...getRootProps()} | ||
className={` | ||
relative group cursor-pointer | ||
border-2 border-dashed rounded-lg overflow-hidden | ||
transition-colors duration-200 | ||
${isDragActive ? 'border-primary bg-primary/5' : 'border-gray-200'} | ||
${isUploading ? 'pointer-events-none' : ''} | ||
`} | ||
> | ||
<input {...getInputProps()} /> | ||
|
||
{isUploading && ( | ||
<div className="absolute inset-0 bg-background/50 backdrop-blur-sm flex items-center justify-center z-10"> | ||
<Loader2 className="w-6 h-6 animate-spin text-primary" /> | ||
</div> | ||
)} | ||
|
||
{image ? ( | ||
<div className="relative group"> | ||
<img src={image} alt="Uploaded content" className="w-full h-48 object-cover" /> | ||
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"> | ||
<p className="text-white text-sm">Click or drag to replace</p> | ||
</div> | ||
</div> | ||
) : ( | ||
<div className="w-full h-48 flex flex-col items-center justify-center gap-2 text-muted-foreground"> | ||
{isDragActive ? ( | ||
<p className="text-primary">Drop the image here...</p> | ||
) : ( | ||
<> | ||
<p>Click or drag image to upload</p> | ||
<p className="text-xs">PNG, JPG, GIF up to 10MB</p> | ||
</> | ||
)} | ||
</div> | ||
)} | ||
</div> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
|
||
interface PresignedResponse { | ||
url: string; | ||
file: string; | ||
} | ||
|
||
class UploadError extends Error { | ||
constructor( | ||
message: string, | ||
public fileName: string, | ||
) { | ||
super(message); | ||
this.name = 'UploadError'; | ||
} | ||
} | ||
|
||
export interface UploadedFile { | ||
name: string; | ||
url: string; | ||
type: string; | ||
} | ||
|
||
export async function getPresignedUrl(filename: string): Promise<PresignedResponse> { | ||
const response = await fetch(`/api/pre-signed`, { | ||
method: 'POST', | ||
body: JSON.stringify({ filename }), | ||
}); | ||
|
||
if (!response.ok) { | ||
throw new UploadError(`Failed to get presigned URL`, filename); | ||
} | ||
|
||
return response.json(); | ||
} | ||
|
||
export async function uploadFileToR2(presignedUrl: string, file: File): Promise<void> { | ||
const response = await fetch(presignedUrl, { | ||
method: 'PUT', | ||
headers: { | ||
'Content-Type': file.type, | ||
}, | ||
body: file, | ||
}); | ||
|
||
if (!response.ok) { | ||
throw new UploadError('Upload failed', file.name); | ||
} | ||
} | ||
|
||
export async function uploadSingleFile(file: File): Promise<UploadedFile> { | ||
const presignedData = await getPresignedUrl(file.name); | ||
await uploadFileToR2(presignedData.url, file); | ||
return { | ||
name: file.name, | ||
url: `https://image.memfree.me/${presignedData.file}`, | ||
type: file.type, | ||
}; | ||
} |