From 77f6b5c5e263d79cbbdc56bd34be4f958819b358 Mon Sep 17 00:00:00 2001 From: Ultradesu Date: Mon, 18 May 2026 14:47:04 +0300 Subject: [PATCH] Added SEO keywords. Added medua upload indicator. Added Khabarovsk default --- Cargo.toml | 2 +- src/admin.rs | 6 +- src/i18n.rs | 5 +- src/public.rs | 8 ++ templates/admin/leads.html | 6 -- templates/admin/media_upload.html | 80 ++++++++++++++- templates/admin/schedule.html | 25 ++--- templates/admin/schedule_edit.html | 159 ++++++++++++++++++++++++++--- templates/admin/settings.html | 57 +++++++++++ templates/landing.html | 11 ++ 10 files changed, 319 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0eb8cc2..0ea531d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "web-petting" -version = "0.1.8" +version = "0.1.9" edition = "2024" [dependencies] diff --git a/src/admin.rs b/src/admin.rs index c36cb13..05f0878 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -796,6 +796,7 @@ struct SettingsForm { pricing_info: String, timezone: String, site_domain: String, + seo_keywords: String, } async fn save_settings(request: Request, session: Session, db: Database) -> cot::Result { @@ -811,6 +812,7 @@ async fn save_settings(request: Request, session: Session, db: Database) -> cot: ("pricing_info", form.pricing_info), ("timezone", form.timezone), ("site_domain", form.site_domain), + ("seo_keywords", form.seo_keywords), ] { let k = key.to_string(); let existing = query!(Setting, $key == k).get(&db).await?; @@ -1172,7 +1174,7 @@ async fn schedule_events( .unwrap_or("?"); let (bg_color, text_color) = match v.status.as_str() { - "cancelled" => ("#ccc".to_string(), "#666"), + "cancelled" => ("#ffb3b3".to_string(), "#a00"), "completed" => (format!("{}88", client_color), "#fff"), _ => (client_color.to_string(), "#fff"), }; @@ -1641,8 +1643,10 @@ async fn media_delete( .and_then(|v| v.to_str().ok()) .map(|s| s.to_string()); if let Some(mut m) = query!(Media, $id == media_id).get(&db).await? { + let file_path = m.file_path.clone(); m.status = "archived".to_string(); m.save(&db).await?; + let _ = tokio::fs::remove_file(&file_path).await; } let redirect_url = referer .filter(|r| r.contains("/schedule/") && r.contains("/edit")) diff --git a/src/i18n.rs b/src/i18n.rs index ed8b7b5..c930fbc 100644 --- a/src/i18n.rs +++ b/src/i18n.rs @@ -134,6 +134,7 @@ pub struct Translations { pub settings_pricing_info: &'static str, pub settings_timezone: &'static str, pub settings_site_domain: &'static str, + pub settings_seo_keywords: &'static str, pub landing_contact_label: &'static str, pub landing_pricing_title: &'static str, @@ -346,6 +347,7 @@ static RU: Translations = Translations { settings_pricing_info: "Блок с ценами (отображается на лендинге)", settings_timezone: "Часовой пояс (например Asia/Vladivostok)", settings_site_domain: "Домен сайта (например https://example.com)", + settings_seo_keywords: "SEO-ключевые слова (через запятую, отображаются на сайте и в мета-теге keywords)", landing_contact_label: "Или свяжитесь с нами напрямую", landing_pricing_title: "Стоимость", @@ -425,7 +427,7 @@ static RU: Translations = Translations { schedule_delete_confirm: "Точно удалить этот визит?", landing_meta_description: "Профессиональный пет-ситтинг: кормление и уход за кошками, грызунами, рептилиями на вашей территории. Оставьте заявку — позаботимся о вашем любимце!", - landing_hero_title: "Позаботимся о вашем питомце, пока вас нет дома", + landing_hero_title: "Позаботимся о вашем питомце, пока вас нет дома. Город Хабаровск", landing_hero_subtitle: "Кормление и уход за кошками, грызунами, рептилиями на вашей территории. Ежедневные визиты — ваш питомец в надёжных руках, пока вы в отпуске или командировке.", landing_hero_description: "Почему лучше оставить кошку дома на время отъезда, чем, скажем, поместить в зоогостиницу? Как известно — кошка территориальное животное. Поэтому, когда кошка оказывается на незнакомой территории — она может испытывать стресс. К тому же в зоогостинице животное часто содержится в клетке. А кошки любят свободу. И дома ожидать своих хозяев — ей будет гораздо проще и комфортнее.", landing_hero_cta: "Оставить заявку", @@ -548,6 +550,7 @@ static EN: Translations = Translations { settings_pricing_info: "Pricing block (shown on landing page)", settings_timezone: "Timezone (e.g. Asia/Vladivostok)", settings_site_domain: "Site domain (e.g. https://example.com)", + settings_seo_keywords: "SEO keywords (comma-separated, shown on site and in keywords meta tag)", landing_contact_label: "Or contact us directly", landing_pricing_title: "Pricing", diff --git a/src/public.rs b/src/public.rs index 8b33c3d..6706116 100644 --- a/src/public.rs +++ b/src/public.rs @@ -71,6 +71,7 @@ struct LandingTemplate<'a> { lang: Lang, contact_info: String, pricing_info: String, + seo_keywords: String, testimonials: Vec, site_domain: String, review_count: usize, @@ -103,6 +104,12 @@ async fn landing_page(request: Request, db: Database) -> cot::Result { .await? .map(|s| s.value) .unwrap_or_else(|| "https://example.net".to_string()); + let seo_key = "seo_keywords".to_string(); + let seo_keywords = query!(Setting, $key == seo_key) + .get(&db) + .await? + .map(|s| s.value) + .unwrap_or_default(); let mut testimonials = Testimonial::objects().all(&db).await?; testimonials.retain(|t| t.status == "active"); testimonials.sort_by(|a, b| a.sort_order.cmp(&b.sort_order)); @@ -112,6 +119,7 @@ async fn landing_page(request: Request, db: Database) -> cot::Result { lang, contact_info, pricing_info, + seo_keywords, testimonials, site_domain, review_count, diff --git a/templates/admin/leads.html b/templates/admin/leads.html index 04a20db..cd6a97d 100644 --- a/templates/admin/leads.html +++ b/templates/admin/leads.html @@ -33,12 +33,6 @@ {% if lead.status == "new" || lead.status == "in_progress" %}
- {% if lead.status == "new" %} -
- - -
- {% endif %}
diff --git a/templates/admin/media_upload.html b/templates/admin/media_upload.html index a012fef..b811f47 100644 --- a/templates/admin/media_upload.html +++ b/templates/admin/media_upload.html @@ -14,12 +14,13 @@
{{ t.schedule_date }}: {{ visit_label }}
-
+
- +
+

@@ -29,7 +30,80 @@
- + + + +
+ + {% endblock %} diff --git a/templates/admin/schedule.html b/templates/admin/schedule.html index 64a6027..7c8938d 100644 --- a/templates/admin/schedule.html +++ b/templates/admin/schedule.html @@ -32,6 +32,10 @@ .fc .fc-toolbar-title { font-size: 1.1rem !important; } .fc .fc-button { padding: 0.25rem 0.5rem !important; font-size: 0.8rem !important; } .fc-event { cursor: pointer; border: none !important; padding: 2px 5px; border-radius: 4px; } + .fc-event.ev-completed .fc-event-title, + .fc-event.ev-completed .fc-list-event-title { text-decoration: line-through; opacity: 0.75; } + .fc-event.ev-cancelled .fc-event-title, + .fc-event.ev-cancelled .fc-list-event-title { text-decoration: line-through; } .fc .fc-day-today { background: #eef2ff !important; } .fc .fc-day.day-weekend { background: #faf5f0; } .fc .fc-day-today.day-weekend { background: #eef2ff !important; } @@ -45,8 +49,6 @@ .visit-modal { background:#fff; border-radius:12px; padding:1.5rem; width:90%; max-width:380px; box-shadow:0 4px 24px rgba(0,0,0,0.15); } .visit-modal h3 { margin:0 0 0.75rem; font-size:1.1rem; } .visit-modal .meta { color:#888; font-size:0.85rem; margin-bottom:0.75rem; line-height:1.6; } - .visit-modal .actions { display:flex; gap:0.5rem; flex-wrap:wrap; } - .visit-modal .actions form { margin:0; } .color-dot { display:inline-block; width:12px; height:12px; border-radius:50%; margin-right:6px; vertical-align:middle; } @@ -60,12 +62,8 @@
-
-
- +
+ 📋 {{ t.schedule_edit_title }} @@ -89,6 +87,11 @@ document.addEventListener('DOMContentLoaded', function() { right: 'dayGridMonth,timeGridWeek,timeGridDay,listWeek' }, events: '/admin/schedule/events', + eventDidMount: function(info) { + var status = info.event.extendedProps.status; + if (status === 'completed') info.el.classList.add('ev-completed'); + if (status === 'cancelled') info.el.classList.add('ev-cancelled'); + }, eventClick: function(info) { info.jsEvent.preventDefault(); const ev = info.event; @@ -102,12 +105,6 @@ document.addEventListener('DOMContentLoaded', function() { document.getElementById('vmNotes').textContent = p.notes || ''; const badge = '' + statusLabels[p.status] + ''; document.getElementById('vmStatus').innerHTML = badge; - let actions = ''; - if (p.status === 'scheduled') { - actions += '
'; - actions += '
'; - } - document.getElementById('vmActions').innerHTML = actions; document.getElementById('vmEditLink').href = '/admin/schedule/' + ev.id + '/edit?lang=' + lang; document.getElementById('visitModal').classList.add('is-open'); }, diff --git a/templates/admin/schedule_edit.html b/templates/admin/schedule_edit.html index 986c028..c13a31c 100644 --- a/templates/admin/schedule_edit.html +++ b/templates/admin/schedule_edit.html @@ -70,14 +70,17 @@
-
-
- -
+ +
+ + +
@@ -150,14 +153,15 @@

{{ t.media_upload_title }}

- +
-
+
- +
+

@@ -165,12 +169,56 @@
- + + + + +
+ + {% endblock %} diff --git a/templates/admin/settings.html b/templates/admin/settings.html index f2bf9c5..d66585b 100644 --- a/templates/admin/settings.html +++ b/templates/admin/settings.html @@ -44,7 +44,64 @@ +
+ +
+ +
+ +

Каждая фраза между запятыми — отдельное ключевое слово

+
+ + + {% endblock %} diff --git a/templates/landing.html b/templates/landing.html index 392b46a..fe9f251 100644 --- a/templates/landing.html +++ b/templates/landing.html @@ -21,6 +21,10 @@ + {% if !seo_keywords.is_empty() %} + + {% endif %} + @@ -301,6 +305,10 @@ border-top: 1px solid rgba(180,170,220,0.15); background: rgba(255,255,255,0.3); } + .seo-keywords { + font-size: 0.72rem; color: #aaa; line-height: 2; + max-width: 800px; margin: 0 auto 1rem; + } /* ── Mobile ── */ @media (max-width: 600px) { @@ -469,6 +477,9 @@