Spaces:
Sleeping
Sleeping
| 'use client'; | |
| import { useState, useRef } from 'react'; | |
| import styles from './UploadZone.module.css'; | |
| const resizeImage = (file, maxSize = 512) => { | |
| return new Promise((resolve) => { | |
| const img = new Image(); | |
| img.onload = () => { | |
| let { width, height } = img; | |
| if (width > height) { | |
| if (width > maxSize) { | |
| height = Math.round((height * maxSize) / width); | |
| width = maxSize; | |
| } | |
| } else { | |
| if (height > maxSize) { | |
| width = Math.round((width * maxSize) / height); | |
| height = maxSize; | |
| } | |
| } | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = width; | |
| canvas.height = height; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(img, 0, 0, width, height); | |
| canvas.toBlob((blob) => { | |
| resolve({ blob, preview: URL.createObjectURL(blob) }); | |
| }, 'image/png'); | |
| }; | |
| img.src = URL.createObjectURL(file); | |
| }); | |
| }; | |
| export default function UploadZone({ status, selectedImage, onImageSelect, onSubmit, errorMessage }) { | |
| const [isDragging, setIsDragging] = useState(false); | |
| const fileInputRef = useRef(null); | |
| const handleDragEnter = (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| setIsDragging(true); | |
| }; | |
| const handleDragOver = (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| setIsDragging(true); | |
| }; | |
| const handleDragLeave = (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| if (!e.currentTarget.contains(e.relatedTarget)) { | |
| setIsDragging(false); | |
| } | |
| }; | |
| const processFile = async (file) => { | |
| if (!file) return; | |
| const allowed = ['image/png', 'image/jpeg', 'image/webp', 'image/gif']; | |
| if (!allowed.includes(file.type)) { | |
| onImageSelect(null, "Invalid file type. Use PNG, JPEG, WebP, or GIF."); | |
| return; | |
| } | |
| if (file.size > 10 * 1024 * 1024) { | |
| onImageSelect(null, "File is too large. Max 10MB allowed."); | |
| return; | |
| } | |
| try { | |
| const resized = await resizeImage(file, 512); | |
| onImageSelect(resized.blob, null, resized.preview); | |
| } catch (err) { | |
| onImageSelect(null, "Failed to process image."); | |
| } | |
| }; | |
| const handleDrop = (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| setIsDragging(false); | |
| if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { | |
| processFile(e.dataTransfer.files[0]); | |
| } else if (e.dataTransfer.items && e.dataTransfer.items.length > 0) { | |
| const item = e.dataTransfer.items[0]; | |
| if (item.kind === 'file') { | |
| const file = item.getAsFile(); | |
| processFile(file); | |
| } | |
| } | |
| if (e.dataTransfer.clearData) { | |
| e.dataTransfer.clearData(); | |
| } | |
| }; | |
| const handleFileChange = (e) => { | |
| if (e.target.files && e.target.files.length > 0) { | |
| processFile(e.target.files[0]); | |
| } | |
| }; | |
| if (status === 'loading' || status === 'success') { | |
| return null; | |
| } | |
| return ( | |
| <div className={styles.uploadContainer}> | |
| <div | |
| className={`glass-panel ${styles.dropZone} ${isDragging ? styles.dragging : ''} ${errorMessage ? styles.errorShake : ''}`} | |
| onDragEnter={handleDragEnter} | |
| onDragOver={handleDragOver} | |
| onDragLeave={handleDragLeave} | |
| onDrop={handleDrop} | |
| onClick={() => !selectedImage && fileInputRef.current?.click()} | |
| > | |
| <input | |
| type="file" | |
| ref={fileInputRef} | |
| hidden | |
| accept="image/png, image/jpeg, image/webp, image/gif" | |
| onChange={handleFileChange} | |
| /> | |
| {selectedImage ? ( | |
| <div className={styles.previewContainer}> | |
| {/* eslint-disable-next-line @next/next/no-img-element */} | |
| <img src={selectedImage.preview} alt="Preview" className={styles.previewImage} /> | |
| <div className={styles.actions}> | |
| <button | |
| className={styles.resetBtn} | |
| onClick={(e) => { e.stopPropagation(); onImageSelect(null, null, null); }} | |
| > | |
| ✕ Try another image | |
| </button> | |
| </div> | |
| </div> | |
| ) : ( | |
| <div className={styles.promptContent}> | |
| <div className={styles.icon}> | |
| <svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> | |
| <polyline points="17 8 12 3 7 8"></polyline> | |
| <line x1="12" y1="3" x2="12" y2="15"></line> | |
| </svg> | |
| </div> | |
| <h3>Drop image here</h3> | |
| <p className={styles.hint}>or click to upload</p> | |
| </div> | |
| )} | |
| </div> | |
| {errorMessage && ( | |
| <div className={styles.errorMessage}>{errorMessage}</div> | |
| )} | |
| {selectedImage && ( | |
| <button | |
| className={`btn glow-primary ${styles.submitBtn}`} | |
| onClick={onSubmit} | |
| > | |
| Identify Pokémon! | |
| </button> | |
| )} | |
| </div> | |
| ); | |
| } | |