feat: upload drag-drop + responsive mobile
Backend: - Route POST /api/upload avec multipart (axum 0.8) - Proxy vers Imago, validation MIME, feedback JSON Frontend: - Zone de drop avec drag & drop (effet visuel au survol) - Bouton Upload dans le header + FAB flottant mobile - Support Ctrl+V (coller depuis presse-papier) - Upload multiple, feedback toast (succès/erreur) - Raccourci Ctrl+U pour upload - Responsive: 2 breakpoints (768px, 400px) - Gallery 2 colonnes sur mobile, FAB, toasts bottom - user-scalable=no, tap-highlight transparent
This commit is contained in:
+62
-2
@@ -1,8 +1,8 @@
|
||||
//! Routes API REST du dashboard — proxy vers Imago v2
|
||||
|
||||
use axum::{
|
||||
Router, routing::get,
|
||||
extract::{Path, Query, State},
|
||||
Router, routing::{get, post},
|
||||
extract::{Multipart, Path, Query, State},
|
||||
response::IntoResponse,
|
||||
Json,
|
||||
};
|
||||
@@ -55,6 +55,7 @@ pub fn api_routes(state: AppState) -> Router {
|
||||
let state = Arc::new(state);
|
||||
Router::new()
|
||||
.route("/api/health", get(health_check))
|
||||
.route("/api/upload", post(upload_image))
|
||||
.route("/api/images", get(list_images))
|
||||
.route("/api/images/search", get(search_images))
|
||||
.route("/api/images/{id}", get(get_image))
|
||||
@@ -69,6 +70,65 @@ async fn health_check() -> &'static str {
|
||||
"OK"
|
||||
}
|
||||
|
||||
/// Upload d'image : reçoit un fichier et le transmet à l'API Imago
|
||||
async fn upload_image(
|
||||
State(state): State<Arc<AppState>>,
|
||||
mut multipart: Multipart,
|
||||
) -> impl IntoResponse {
|
||||
let mut data: Vec<u8> = Vec::new();
|
||||
let mut filename = String::from("upload.png");
|
||||
let mut mime_type = String::from("image/png");
|
||||
|
||||
while let Ok(Some(mut field)) = multipart.next_field().await {
|
||||
let name = field.name().unwrap_or("").to_string();
|
||||
if name == "file" {
|
||||
if let Some(fn_) = field.file_name() {
|
||||
filename = fn_.to_string();
|
||||
}
|
||||
if let Some(mt) = field.content_type() {
|
||||
mime_type = mt.to_string();
|
||||
}
|
||||
while let Ok(Some(chunk)) = field.chunk().await {
|
||||
data.extend_from_slice(&chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if data.is_empty() {
|
||||
return (
|
||||
axum::http::StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({"error": "Aucun fichier reçu"})),
|
||||
);
|
||||
}
|
||||
|
||||
// Validation MIME basique côté dashboard
|
||||
if !mime_type.starts_with("image/") {
|
||||
return (
|
||||
axum::http::StatusCode::UNSUPPORTED_MEDIA_TYPE,
|
||||
Json(serde_json::json!({"error": format!("Type MIME non supporté : {}", mime_type)})),
|
||||
);
|
||||
}
|
||||
|
||||
match state.imago.upload_image(data, &filename, None).await {
|
||||
Ok(response) => (
|
||||
axum::http::StatusCode::CREATED,
|
||||
Json(serde_json::json!({
|
||||
"id": response.id,
|
||||
"uuid": response.uuid,
|
||||
"filename": response.original_name,
|
||||
"status": response.status,
|
||||
})),
|
||||
),
|
||||
Err(e) => {
|
||||
tracing::error!("Upload vers Imago échoué : {:#}", e);
|
||||
(
|
||||
axum::http::StatusCode::BAD_GATEWAY,
|
||||
Json(serde_json::json!({"error": format!("Échec de l'upload : {:#}", e)})),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_images(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(pagination): Query<Pagination>,
|
||||
|
||||
Reference in New Issue
Block a user