upload multiple files
This commit is contained in:
Binary file not shown.
@@ -46,20 +46,39 @@ def delete_celebrity(celebrity_id: int, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
# --- NUOVI ENDPOINT PER LE IMMAGINI ---
|
# --- NUOVI ENDPOINT PER LE IMMAGINI ---
|
||||||
|
|
||||||
@router.post("/{celebrity_id}/images", response_model=schemas.Image, status_code=201)
|
@router.post("/{celebrity_id}/images", response_model=List[schemas.Image], status_code=201)
|
||||||
def upload_celebrity_image(celebrity_id: int, file: UploadFile = File(...), db: Session = Depends(get_db)):
|
def upload_celebrity_images(celebrity_id: int, files: List[UploadFile] = File(...), db: Session = Depends(get_db)):
|
||||||
db_celebrity = crud.get_celebrity(db, celebrity_id=celebrity_id)
|
db_celebrity = crud.get_celebrity(db, celebrity_id=celebrity_id)
|
||||||
if db_celebrity is None:
|
if db_celebrity is None:
|
||||||
raise HTTPException(status_code=404, detail="Celebrity not found")
|
raise HTTPException(status_code=404, detail="Celebrity not found")
|
||||||
|
|
||||||
file_extension = file.filename.split('.')[-1]
|
created_images = []
|
||||||
unique_filename = f"{uuid.uuid4()}.{file_extension}"
|
for file in files:
|
||||||
file_location = f"uploads/{unique_filename}"
|
try:
|
||||||
|
file_extension = file.filename.split('.')[-1]
|
||||||
|
if not file_extension: # Gestisce file senza estensione
|
||||||
|
file_extension = "jpg" # Default
|
||||||
|
|
||||||
|
unique_filename = f"{uuid.uuid4()}.{file_extension}"
|
||||||
|
file_location = f"uploads/{unique_filename}"
|
||||||
|
|
||||||
with open(file_location, "wb+") as file_object:
|
with open(file_location, "wb+") as file_object:
|
||||||
shutil.copyfileobj(file.file, file_object)
|
shutil.copyfileobj(file.file, file_object)
|
||||||
|
|
||||||
|
db_image = crud.create_celebrity_image(db=db, celebrity_id=celebrity_id, file_path=unique_filename)
|
||||||
|
created_images.append(db_image)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# Se un file fallisce, potremmo voler continuare con gli altri,
|
||||||
|
# ma per ora lanciamo un'eccezione per l'intero batch.
|
||||||
|
# In un'app di produzione, si potrebbe restituire una risposta parziale.
|
||||||
|
print(f"Failed to upload file {file.filename}: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail=f"Could not upload file: {file.filename}")
|
||||||
|
|
||||||
return crud.create_celebrity_image(db=db, celebrity_id=celebrity_id, file_path=unique_filename)
|
if not created_images:
|
||||||
|
raise HTTPException(status_code=400, detail="No files were successfully uploaded.")
|
||||||
|
|
||||||
|
return created_images
|
||||||
|
|
||||||
@router.post("/{celebrity_id}/images/from-url", response_model=schemas.Image, status_code=201)
|
@router.post("/{celebrity_id}/images/from-url", response_model=schemas.Image, status_code=201)
|
||||||
async def add_image_from_url(celebrity_id: int, request: schemas.ImageUrlRequest, db: Session = Depends(get_db)):
|
async def add_image_from_url(celebrity_id: int, request: schemas.ImageUrlRequest, db: Session = Depends(get_db)):
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||||
import { getCelebrityById, updateCelebrity, deleteCelebrity, uploadImage, setProfileImage, deleteImage, addImageFromUrl } from '../services/api';
|
import { getCelebrityById, updateCelebrity, deleteCelebrity, uploadImages, setProfileImage, deleteImage, addImageFromUrl } from '../services/api';
|
||||||
import EditableField from './EditableField';
|
import EditableField from './EditableField';
|
||||||
import './CelebrityProfile.css';
|
import './CelebrityProfile.css';
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ function CelebrityProfile() {
|
|||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
|
|
||||||
// Stati per la gestione upload
|
// Stati per la gestione upload
|
||||||
const [selectedFile, setSelectedFile] = useState(null);
|
const [selectedFiles, setSelectedFiles] = useState(null); // Da file a files
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
const [imageUrl, setImageUrl] = useState('');
|
const [imageUrl, setImageUrl] = useState('');
|
||||||
const [isFetchingFromUrl, setIsFetchingFromUrl] = useState(false);
|
const [isFetchingFromUrl, setIsFetchingFromUrl] = useState(false);
|
||||||
@@ -43,12 +43,13 @@ function CelebrityProfile() {
|
|||||||
}
|
}
|
||||||
}, [id, navigate, fetchProfile]);
|
}, [id, navigate, fetchProfile]);
|
||||||
|
|
||||||
// Effetto per caricare il file quando `selectedFile` cambia
|
// Effetto per caricare i file quando `selectedFiles` cambia
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedFile) {
|
if (selectedFiles && selectedFiles.length > 0) {
|
||||||
handleUpload();
|
handleUpload();
|
||||||
}
|
}
|
||||||
}, [selectedFile]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedFiles]);
|
||||||
|
|
||||||
const handleFieldSave = async (fieldName, newValue) => {
|
const handleFieldSave = async (fieldName, newValue) => {
|
||||||
let valueToSend = newValue === '' ? null : newValue;
|
let valueToSend = newValue === '' ? null : newValue;
|
||||||
@@ -77,22 +78,26 @@ function CelebrityProfile() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Gestori per le Immagini ---
|
// --- Gestori per le Immagini ---
|
||||||
const handleFileChange = (e) => setSelectedFile(e.target.files[0]);
|
const handleFileChange = (e) => {
|
||||||
|
if (e.target.files.length > 0) {
|
||||||
|
setSelectedFiles(Array.from(e.target.files));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleUpload = async () => {
|
const handleUpload = async () => {
|
||||||
if (!selectedFile) return;
|
if (!selectedFiles || selectedFiles.length === 0) return;
|
||||||
setIsUploading(true);
|
setIsUploading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
await uploadImage(id, selectedFile);
|
await uploadImages(id, selectedFiles); // Chiama la nuova funzione plurale
|
||||||
setSelectedFile(null);
|
fetchProfile();
|
||||||
document.querySelector('input[type="file"]').value = ""; // Reset del campo file
|
|
||||||
fetchProfile(); // Ricarica i dati per vedere la nuova immagine
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(`Upload fallito: ${err.message}`);
|
setError(`Upload fallito: ${err.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
|
setSelectedFiles(null);
|
||||||
|
if(fileInputRef.current) fileInputRef.current.value = "";
|
||||||
setIsUploading(false);
|
setIsUploading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -142,7 +147,7 @@ function CelebrityProfile() {
|
|||||||
setIsDraggingOver(false);
|
setIsDraggingOver(false);
|
||||||
const files = e.dataTransfer.files;
|
const files = e.dataTransfer.files;
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
setSelectedFile(files[0]);
|
setSelectedFiles(Array.from(files));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -177,7 +182,7 @@ function CelebrityProfile() {
|
|||||||
|
|
||||||
<main className="profile-content">
|
<main className="profile-content">
|
||||||
{error && <p className="error-message"><strong>Errore:</strong> {error}</p>}
|
{error && <p className="error-message"><strong>Errore:</strong> {error}</p>}
|
||||||
|
|
||||||
<details open>
|
<details open>
|
||||||
<summary><h3>Galleria e Upload</h3></summary>
|
<summary><h3>Galleria e Upload</h3></summary>
|
||||||
<div className="gallery-grid">
|
<div className="gallery-grid">
|
||||||
@@ -207,8 +212,11 @@ function CelebrityProfile() {
|
|||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
aria-busy={isUploading}
|
aria-busy={isUploading}
|
||||||
>
|
>
|
||||||
<input type="file" ref={fileInputRef} onChange={handleFileChange} hidden />
|
<input type="file" ref={fileInputRef} onChange={handleFileChange} hidden multiple />
|
||||||
{isUploading ? <p>Caricamento...</p> : <p>Trascina un file qui, o <strong>clicca per selezionare</strong>.</p>}
|
{isUploading
|
||||||
|
? <p>Caricamento di {selectedFiles.length} file...</p>
|
||||||
|
: <p>Trascina i file qui, o <strong>clicca per selezionare</strong>.</p>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -251,7 +259,7 @@ function CelebrityProfile() {
|
|||||||
<EditableField label="Corporatura" name="body_type" value={celebrity.body_type} onSave={handleFieldSave} />
|
<EditableField label="Corporatura" name="body_type" value={celebrity.body_type} onSave={handleFieldSave} />
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><h3>Misure Corporee</h3></summary>
|
<summary><h3>Misure Corporee</h3></summary>
|
||||||
<div className="profile-grid">
|
<div className="profile-grid">
|
||||||
@@ -277,7 +285,7 @@ function CelebrityProfile() {
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><h3>Biografia</h3></summary>
|
<summary><h3>Biografia</h3></summary>
|
||||||
<EditableField label="Note biografiche" name="biography" value={celebrity.biography} type="textarea" onSave={handleFieldSave} />
|
<EditableField label="Note biografiche" name="biography" value={celebrity.biography} type="textarea" onSave={handleFieldSave} />
|
||||||
|
|||||||
@@ -60,6 +60,20 @@ export const uploadImage = async (celebrityId, file) => {
|
|||||||
return handleResponse(response);
|
return handleResponse(response);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const uploadImages = async (celebrityId, files) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
// Aggiunge ogni file allo stesso campo 'files'. Il backend lo interpreterà come una lista.
|
||||||
|
files.forEach(file => {
|
||||||
|
formData.append('files', file);
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/celebrities/${celebrityId}/images`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
});
|
||||||
|
return handleResponse(response);
|
||||||
|
};
|
||||||
|
|
||||||
export const setProfileImage = async (celebrityId, imageId) => {
|
export const setProfileImage = async (celebrityId, imageId) => {
|
||||||
const response = await fetch(`${API_BASE_URL}/celebrities/${celebrityId}/profile-image`, {
|
const response = await fetch(`${API_BASE_URL}/celebrities/${celebrityId}/profile-image`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
|
|||||||
BIN
uploads/165fa68d-eb00-47f7-a1b2-4d5fa8352728.png
Normal file
BIN
uploads/165fa68d-eb00-47f7-a1b2-4d5fa8352728.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
BIN
uploads/1d024628-4db7-4333-8b77-6e3e61de5a20.webp
Normal file
BIN
uploads/1d024628-4db7-4333-8b77-6e3e61de5a20.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 77 KiB |
BIN
uploads/34da28b0-5f05-4996-acf3-fbeec43fe916.png
Normal file
BIN
uploads/34da28b0-5f05-4996-acf3-fbeec43fe916.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
BIN
uploads/dfdcb6c6-68c4-4dc0-a15e-889df4356e3e.png
Normal file
BIN
uploads/dfdcb6c6-68c4-4dc0-a15e-889df4356e3e.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 266 KiB |
BIN
uploads/ecb201fe-6968-4891-99d6-deafe7fca852.png
Normal file
BIN
uploads/ecb201fe-6968-4891-99d6-deafe7fca852.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.4 MiB |
Reference in New Issue
Block a user