diff --git a/Cargo.toml b/Cargo.toml index 29c7365..4f0223e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "web-petting" -version = "0.1.2" +version = "0.1.3" edition = "2024" [dependencies] diff --git a/src/admin.rs b/src/admin.rs index 7a763f6..25ba67a 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -1805,6 +1805,112 @@ async fn testimonial_delete( Redirect::new(format!("/admin/testimonials?lang={}", lang.code())).into_response() } +async fn testimonial_edit( + request: Request, + session: Session, + db: Database, + Path(id): Path, +) -> cot::Result { + let lang = detect_lang(&request); + if let Err(resp) = require_auth(&session, lang).await { + return Ok(resp); + } + + let boundary = extract_boundary(&request) + .ok_or_else(|| cot::Error::internal("missing multipart boundary"))?; + + let bytes = request.into_body().into_bytes().await?; + let stream = + futures::stream::once(async move { Result::<_, std::convert::Infallible>::Ok(bytes) }); + let mut multipart = multer::Multipart::new(stream, boundary); + + let mut text = String::new(); + let mut author_note = String::new(); + let mut new_image_path: Option = None; + let mut remove_image = false; + + while let Some(field) = multipart + .next_field() + .await + .map_err(|e| cot::Error::internal(e.to_string()))? + { + let field_name = field.name().unwrap_or("").to_string(); + match field_name.as_str() { + "text" => { + text = field + .text() + .await + .map_err(|e| cot::Error::internal(e.to_string()))?; + } + "author_note" => { + author_note = field + .text() + .await + .map_err(|e| cot::Error::internal(e.to_string()))?; + } + "remove_image" => { + let val = field + .text() + .await + .map_err(|e| cot::Error::internal(e.to_string()))?; + remove_image = val == "1"; + } + "image" => { + let original_name = field.file_name().unwrap_or("").to_string(); + if original_name.is_empty() { + continue; + } + let ext = original_name + .rsplit('.') + .next() + .unwrap_or("bin") + .to_lowercase(); + match ext.as_str() { + "jpg" | "jpeg" | "png" | "webp" | "heic" | "heif" => {} + _ => continue, + } + let data = field + .bytes() + .await + .map_err(|e| cot::Error::internal(e.to_string()))?; + if data.is_empty() { + continue; + } + let upload_dir = "uploads/testimonials"; + tokio::fs::create_dir_all(upload_dir) + .await + .map_err(|e| cot::Error::internal(e.to_string()))?; + let file_id = uuid::Uuid::new_v4(); + let path = format!("{}/{}.{}", upload_dir, file_id, ext); + tokio::fs::write(&path, &data) + .await + .map_err(|e| cot::Error::internal(e.to_string()))?; + new_image_path = Some(path); + } + _ => {} + } + } + + if let Some(mut t) = query!(Testimonial, $id == id).get(&db).await? { + if !text.trim().is_empty() { + t.text = text.trim().to_string(); + } + t.author_note = if author_note.trim().is_empty() { + None + } else { + Some(author_note.trim().to_string()) + }; + if let Some(path) = new_image_path { + t.image_path = Some(path); + } else if remove_image { + t.image_path = None; + } + t.save(&db).await?; + } + + Redirect::new(format!("/admin/testimonials?lang={}", lang.code())).into_response() +} + /// Serve testimonial images (public, no auth needed for landing page). async fn serve_testimonial_image( _request: Request, @@ -1962,6 +2068,11 @@ pub fn admin_router() -> Router { testimonial_delete, "admin-testimonial-delete", ), + Route::with_handler_and_name( + "/testimonials/{id}/edit", + testimonial_edit, + "admin-testimonial-edit", + ), Route::with_handler_and_name( "/testimonials/{id}/image", serve_testimonial_image, diff --git a/src/i18n.rs b/src/i18n.rs index 3c91785..cd8a452 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -196,6 +196,9 @@ pub struct Translations { pub testimonials_add_button: &'static str, pub testimonials_status_active: &'static str, pub testimonials_status_hidden: &'static str, + pub testimonials_edit: &'static str, + pub testimonials_save: &'static str, + pub testimonials_remove_image: &'static str, // Client edit pub clients_edit_title: &'static str, @@ -458,6 +461,9 @@ static RU: Translations = Translations { testimonials_add_button: "Добавить", testimonials_status_active: "Отображается", testimonials_status_hidden: "Скрыт", + testimonials_edit: "Редактировать", + testimonials_save: "Сохранить", + testimonials_remove_image: "Удалить фото", no_value: "—", action_convert: "Конвертировать", @@ -653,6 +659,9 @@ static EN: Translations = Translations { testimonials_add_button: "Add", testimonials_status_active: "Visible", testimonials_status_hidden: "Hidden", + testimonials_edit: "Edit", + testimonials_save: "Save", + testimonials_remove_image: "Remove photo", no_value: "—", action_convert: "Convert", diff --git a/templates/admin/testimonials.html b/templates/admin/testimonials.html index 03b1216..0c86d50 100644 --- a/templates/admin/testimonials.html +++ b/templates/admin/testimonials.html @@ -39,36 +39,92 @@

{{ t.testimonials_empty }}

{% else %} {% for item in &testimonials %} -
-
-
- {% if item.image_path.is_some() %} - - {% endif %} -
-
{{ item.text }}
- {% if let Some(note) = item.author_note.as_deref() %} -
{{ note }}
+
+ +
+
+
+ {% if item.image_path.is_some() %} + {% endif %} +
+
{{ item.text }}
+ {% if let Some(note) = item.author_note.as_deref() %} +
{{ note }}
+ {% endif %} +
+ + {% if item.status == "active" %}{{ t.testimonials_status_active }}{% else %}{{ t.testimonials_status_hidden }}{% endif %} + +
+
+ +
+ {% if item.status == "active" %} + + {% else %} + + {% endif %} +
+
+ +
- - {% if item.status == "active" %}{{ t.testimonials_status_active }}{% else %}{{ t.testimonials_status_hidden }}{% endif %} -
-
-
- {% if item.status == "active" %} - - {% else %} - + + +
{% endfor %} {% endif %} + + {% endblock %}