Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bfd0aec56f | |||
| 68d578b29d | |||
| 21331b75a8 | |||
| 6395e36c62 |
@@ -2,3 +2,4 @@
|
||||
/data
|
||||
db.sqlite3
|
||||
/uploads
|
||||
.DS_Store
|
||||
|
||||
Generated
+1
-1
@@ -3255,7 +3255,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "web-petting"
|
||||
version = "0.1.4"
|
||||
version = "0.1.6"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "web-petting"
|
||||
version = "0.1.4"
|
||||
version = "0.1.6"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
+42
-10
@@ -182,7 +182,9 @@ struct DashboardTemplate<'a> {
|
||||
lang: Lang,
|
||||
admin_name: &'a str,
|
||||
today_visits: Vec<TodayVisit>,
|
||||
recent_feedbacks: Vec<RecentFeedback>,
|
||||
feedbacks: Vec<RecentFeedback>,
|
||||
feedback_page: usize,
|
||||
feedback_total_pages: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Template)]
|
||||
@@ -494,13 +496,10 @@ async fn admin_index(request: Request, session: Session, db: Database) -> cot::R
|
||||
.collect();
|
||||
today_visits.sort_by(|a, b| a.visit.time_start.cmp(&b.visit.time_start));
|
||||
|
||||
let week_ago = today - chrono::Duration::days(7);
|
||||
let mut recent_feedbacks: Vec<RecentFeedback> = all_visits
|
||||
let mut all_feedbacks: Vec<RecentFeedback> = all_visits
|
||||
.iter()
|
||||
.filter(|v| {
|
||||
v.user_id.primary_key().unwrap() == user_id
|
||||
&& v.client_feedback.is_some()
|
||||
&& v.updated_at.date() >= week_ago
|
||||
v.user_id.primary_key().unwrap() == user_id && v.client_feedback.is_some()
|
||||
})
|
||||
.map(|v| {
|
||||
let cid: i64 = v.client_id.primary_key().unwrap();
|
||||
@@ -517,14 +516,34 @@ async fn admin_index(request: Request, session: Session, db: Database) -> cot::R
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
recent_feedbacks.sort_by(|a, b| b.visit_date.cmp(&a.visit_date));
|
||||
all_feedbacks.sort_by(|a, b| b.visit_date.cmp(&a.visit_date));
|
||||
|
||||
const PER_PAGE: usize = 10;
|
||||
let feedback_page: usize = request
|
||||
.uri()
|
||||
.query()
|
||||
.and_then(|q| {
|
||||
q.split('&')
|
||||
.find_map(|p| p.strip_prefix("page="))
|
||||
.and_then(|v| v.parse().ok())
|
||||
})
|
||||
.unwrap_or(1)
|
||||
.max(1);
|
||||
let feedback_total_pages = (all_feedbacks.len() + PER_PAGE - 1).max(1) / PER_PAGE.max(1);
|
||||
let feedbacks: Vec<RecentFeedback> = all_feedbacks
|
||||
.into_iter()
|
||||
.skip((feedback_page - 1) * PER_PAGE)
|
||||
.take(PER_PAGE)
|
||||
.collect();
|
||||
|
||||
let body = DashboardTemplate {
|
||||
t: lang.t(),
|
||||
lang,
|
||||
admin_name: &admin_name,
|
||||
today_visits,
|
||||
recent_feedbacks,
|
||||
feedbacks,
|
||||
feedback_page,
|
||||
feedback_total_pages,
|
||||
}
|
||||
.render()?;
|
||||
html_response(body, lang)
|
||||
@@ -1569,7 +1588,12 @@ async fn media_upload_submit(
|
||||
media.save(&db).await?;
|
||||
}
|
||||
|
||||
Redirect::new(format!("/admin/?lang={}", lang.code())).into_response()
|
||||
Redirect::new(format!(
|
||||
"/admin/schedule/{}/edit?lang={}",
|
||||
visit_id,
|
||||
lang.code()
|
||||
))
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn media_delete(
|
||||
@@ -1582,11 +1606,19 @@ async fn media_delete(
|
||||
if let Err(resp) = require_auth(&session, lang).await {
|
||||
return Ok(resp);
|
||||
}
|
||||
let referer = request
|
||||
.headers()
|
||||
.get("referer")
|
||||
.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? {
|
||||
m.status = "archived".to_string();
|
||||
m.save(&db).await?;
|
||||
}
|
||||
Redirect::new(format!("/admin/media?lang={}", lang.code())).into_response()
|
||||
let redirect_url = referer
|
||||
.filter(|r| r.contains("/schedule/") && r.contains("/edit"))
|
||||
.unwrap_or_else(|| format!("/admin/media?lang={}", lang.code()));
|
||||
Redirect::new(redirect_url).into_response()
|
||||
}
|
||||
|
||||
/// Serve uploaded files by media ID.
|
||||
|
||||
+6
-6
@@ -282,7 +282,7 @@ static RU: Translations = Translations {
|
||||
nav_visits: "Визиты",
|
||||
nav_users: "Админы",
|
||||
nav_settings: "Настройки",
|
||||
nav_title: "Пет-ситтинг",
|
||||
nav_title: "МурНяня.РФ",
|
||||
|
||||
leads_title: "Заявки",
|
||||
leads_empty: "Заявок пока нет.",
|
||||
@@ -360,7 +360,7 @@ static RU: Translations = Translations {
|
||||
media_delete_confirm: "Удалить этот файл?",
|
||||
media_all_clients: "Все клиенты",
|
||||
|
||||
portal_title: "Мои визиты",
|
||||
portal_title: "Визиты",
|
||||
portal_upcoming: "Предстоящие визиты",
|
||||
portal_past: "Прошлые визиты",
|
||||
portal_no_upcoming: "Нет предстоящих визитов.",
|
||||
@@ -450,7 +450,7 @@ static RU: Translations = Translations {
|
||||
landing_guarantee: "Порядочность, честность и строгое выполнение ваших требований гарантировано.",
|
||||
landing_testimonials_title: "Отзывы",
|
||||
landing_form_consent: "Я даю согласие на обработку персональных данных",
|
||||
landing_footer_text: "Пет-ситтинг — забота о вашем питомце",
|
||||
landing_footer_text: "МурНяня.РФ — Присмотрим, погладим, покормим",
|
||||
landing_footer_copyright: "Все права защищены",
|
||||
|
||||
nav_testimonials: "Отзывы",
|
||||
@@ -481,7 +481,7 @@ static EN: Translations = Translations {
|
||||
nav_visits: "Visits",
|
||||
nav_users: "Admins",
|
||||
nav_settings: "Settings",
|
||||
nav_title: "Pet Sitting",
|
||||
nav_title: "МурНяня.РФ",
|
||||
|
||||
leads_title: "Leads",
|
||||
leads_empty: "No leads yet.",
|
||||
@@ -559,7 +559,7 @@ static EN: Translations = Translations {
|
||||
media_delete_confirm: "Delete this file?",
|
||||
media_all_clients: "All clients",
|
||||
|
||||
portal_title: "My Visits",
|
||||
portal_title: "Visits",
|
||||
portal_upcoming: "Upcoming visits",
|
||||
portal_past: "Past visits",
|
||||
portal_no_upcoming: "No upcoming visits.",
|
||||
@@ -649,7 +649,7 @@ static EN: Translations = Translations {
|
||||
landing_guarantee: "Integrity, honesty, and strict fulfillment of your requirements guaranteed.",
|
||||
landing_testimonials_title: "Testimonials",
|
||||
landing_form_consent: "I consent to the processing of my personal data",
|
||||
landing_footer_text: "Pet Sitting — caring for your pet",
|
||||
landing_footer_text: "МурНяня.РФ — Присмотрим, погладим, покормим",
|
||||
landing_footer_copyright: "All rights reserved",
|
||||
|
||||
nav_testimonials: "Testimonials",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use chrono::TimeZone;
|
||||
use chrono_tz::Tz;
|
||||
use cot::db::{Database, Model, query};
|
||||
use cot::db::{Database, query};
|
||||
|
||||
use crate::models::Setting;
|
||||
|
||||
|
||||
@@ -47,19 +47,37 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Recent feedbacks -->
|
||||
<!-- Feedbacks -->
|
||||
<h2 style="font-size:1.15rem;font-weight:700;margin:1.5rem 0 0.75rem;">{{ t.dashboard_recent_feedbacks }}</h2>
|
||||
{% if recent_feedbacks.is_empty() %}
|
||||
{% if feedbacks.is_empty() %}
|
||||
<p class="has-text-grey">{{ t.dashboard_no_feedbacks }}</p>
|
||||
{% else %}
|
||||
{% for fb in &recent_feedbacks %}
|
||||
<div class="item-card" style="border-left:3px solid #7c6cff;">
|
||||
{% for fb in &feedbacks %}
|
||||
<a href="/admin/schedule/{{ fb.visit_id }}/edit?lang={{ lang.code() }}" class="item-card" style="border-left:3px solid #7c6cff;display:block;text-decoration:none;color:inherit;">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.25rem;">
|
||||
<strong style="font-size:0.9rem;">{{ fb.client_name }}</strong>
|
||||
<a href="/admin/schedule/{{ fb.visit_id }}/edit?lang={{ lang.code() }}" style="color:#999;font-size:0.8rem;text-decoration:none;">{{ fb.visit_date }}</a>
|
||||
<span style="color:#999;font-size:0.8rem;">{{ fb.visit_date }}</span>
|
||||
</div>
|
||||
<div style="font-size:0.85rem;color:#4a4570;">{{ fb.feedback }}</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
||||
{% if feedback_total_pages > 1 %}
|
||||
<div style="display:flex;justify-content:center;gap:0.5rem;margin-top:1rem;">
|
||||
{% if feedback_page > 1 %}
|
||||
<a href="/admin/?lang={{ lang.code() }}&page={{ feedback_page - 1 }}" class="button is-small is-light">«</a>
|
||||
{% endif %}
|
||||
{% for p in 1..=feedback_total_pages %}
|
||||
{% if p == feedback_page %}
|
||||
<span class="button is-small is-primary">{{ p }}</span>
|
||||
{% else %}
|
||||
<a href="/admin/?lang={{ lang.code() }}&page={{ p }}" class="button is-small is-light">{{ p }}</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if feedback_page < feedback_total_pages %}
|
||||
<a href="/admin/?lang={{ lang.code() }}&page={{ feedback_page + 1 }}" class="button is-small is-light">»</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -97,55 +97,79 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if let Some(fb) = visit.client_feedback.as_deref() %}
|
||||
<div style="margin-bottom:1rem;">
|
||||
<label class="label">{{ t.schedule_client_feedback }}</label>
|
||||
<div style="background:#f0f0ff;border-radius:8px;padding:0.6rem 0.85rem;font-size:0.9rem;color:#4a4570;">{{ fb }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<hr style="margin:1rem 0;">
|
||||
|
||||
<!-- Media -->
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem;">
|
||||
<label class="label" style="margin:0;">{{ t.nav_media }}</label>
|
||||
<button type="button" class="button is-info is-small is-outlined" onclick="document.getElementById('uploadModal').classList.add('is-open')">+ {{ t.media_upload }}</button>
|
||||
</div>
|
||||
{% if media.is_empty() %}
|
||||
<p class="has-text-grey is-size-7" style="margin-bottom:1rem;">{{ t.media_empty }}</p>
|
||||
{% else %}
|
||||
<div class="visit-media-grid">
|
||||
{% for m in &media %}
|
||||
<div class="visit-media-item">
|
||||
{% if m.file_type == "photo" %}
|
||||
<a href="/admin/uploads/{{ m.id }}" data-lightbox="photo">
|
||||
<img src="/admin/uploads/{{ m.id }}" alt="" loading="lazy">
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="/admin/uploads/{{ m.id }}" data-lightbox="video">
|
||||
<div class="video-thumb-sm">🎬</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if let Some(cap) = m.caption.as_deref() %}
|
||||
<div class="media-cap">{{ cap }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<hr style="margin:1rem 0;">
|
||||
|
||||
<button type="submit" class="button is-primary is-fullwidth">{{ t.schedule_save }}</button>
|
||||
</form>
|
||||
|
||||
{% if let Some(fb) = visit.client_feedback.as_deref() %}
|
||||
<div style="margin-top:1rem;">
|
||||
<label class="label">{{ t.schedule_client_feedback }}</label>
|
||||
<div style="background:#f0f0ff;border-radius:8px;padding:0.6rem 0.85rem;font-size:0.9rem;color:#4a4570;">{{ fb }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<hr style="margin:1rem 0;">
|
||||
|
||||
<!-- Media -->
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:0.75rem;">
|
||||
<label class="label" style="margin:0;">{{ t.nav_media }}</label>
|
||||
<a href="/admin/media/{{ visit.id }}/upload?lang={{ lang.code() }}" class="button is-info is-small is-outlined">📷 {{ t.media_upload }}</a>
|
||||
</div>
|
||||
{% if media.is_empty() %}
|
||||
<p class="has-text-grey is-size-7" style="margin-bottom:1rem;">{{ t.media_empty }}</p>
|
||||
{% else %}
|
||||
<div class="visit-media-grid">
|
||||
{% for m in &media %}
|
||||
<div class="visit-media-item">
|
||||
{% if m.file_type == "photo" %}
|
||||
<a href="/admin/uploads/{{ m.id }}" data-lightbox="photo">
|
||||
<img src="/admin/uploads/{{ m.id }}" alt="" loading="lazy">
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="/admin/uploads/{{ m.id }}" data-lightbox="video">
|
||||
<div class="video-thumb-sm">🎬</div>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if let Some(cap) = m.caption.as_deref() %}
|
||||
<div class="media-cap">{{ cap }}</div>
|
||||
{% endif %}
|
||||
<form method="post" action="/admin/media/{{ m.id }}/delete" onsubmit="return confirm('{{ t.media_delete_confirm }}');" style="text-align:center;">
|
||||
<button class="button is-danger is-outlined btn-sm" style="font-size:0.7rem;padding:0.15rem 0.4rem;">{{ t.media_delete }}</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<hr style="margin:1rem 0;">
|
||||
<form method="post" action="/admin/schedule/{{ visit.id }}/delete" onsubmit="return confirm('{{ t.schedule_delete_confirm }}');">
|
||||
<button type="submit" class="button is-danger is-outlined is-fullwidth is-small">{{ t.schedule_delete }}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Upload Modal -->
|
||||
<div class="upload-modal-bg" id="uploadModal">
|
||||
<div class="upload-modal">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;">
|
||||
<h3 style="font-size:1.1rem;font-weight:700;margin:0;">{{ t.media_upload_title }}</h3>
|
||||
<button type="button" style="background:none;border:none;font-size:1.2rem;cursor:pointer;color:#888;" onclick="document.getElementById('uploadModal').classList.remove('is-open')">✕</button>
|
||||
</div>
|
||||
<form method="post" action="/admin/media/{{ visit.id }}/upload/submit" enctype="multipart/form-data">
|
||||
<div class="field">
|
||||
<label class="label">{{ t.media_choose_files }}</label>
|
||||
<div class="control">
|
||||
<input class="input" type="file" name="files" multiple accept="image/*,video/*" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label">{{ t.media_caption }}</label>
|
||||
<div class="control">
|
||||
<input class="input" type="text" name="caption" placeholder="{{ t.media_caption }}">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="button is-primary is-fullwidth">{{ t.media_upload }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.visit-media-grid {
|
||||
display: grid;
|
||||
@@ -182,8 +206,31 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.visit-media-item form {
|
||||
padding: 0.2rem;
|
||||
.upload-modal-bg {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.35);
|
||||
z-index: 100;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.upload-modal-bg.is-open {
|
||||
display: flex;
|
||||
}
|
||||
.upload-modal {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
width: 90%;
|
||||
max-width: 420px;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.15);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.getElementById('uploadModal').addEventListener('click', function(e) {
|
||||
if (e.target === this) this.classList.remove('is-open');
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
+72
-5
@@ -96,6 +96,51 @@
|
||||
box-shadow: 0 4px 20px rgba(124,108,255,0.35);
|
||||
}
|
||||
.hero-cta:hover { transform: translateY(-2px); box-shadow: 0 8px 30px rgba(124,108,255,0.45); }
|
||||
.hero { position: relative; }
|
||||
.hero-blob {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
z-index: 0;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
.hero-blob-br {
|
||||
width: 360px; height: 400px;
|
||||
right: -100px; bottom: -110px;
|
||||
background-color: #ffe0ec;
|
||||
background-image: radial-gradient(ellipse at 40% 40%, #ffd6e8 0%, #f8c4d8 60%, #eab0d0 100%);
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.hero-blob-tl {
|
||||
width: 240px; height: 270px;
|
||||
left: -65px; top: -15px;
|
||||
background-color: #d8ecff;
|
||||
background-image: radial-gradient(ellipse at 60% 60%, #d0e8ff 0%, #b8d8f8 60%, #a4c8f0 100%);
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.hero-blob-bl {
|
||||
width: 290px; height: 330px;
|
||||
left: -80px; bottom: -90px;
|
||||
background-color: #d4f0d8;
|
||||
background-image: radial-gradient(ellipse at 55% 45%, #ddf5e0 0%, #c4e8ca 60%, #b0dbb8 100%);
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.hero-blob svg {
|
||||
position: absolute; opacity: 0.3;
|
||||
}
|
||||
.hero-blob img {
|
||||
width: 100%; height: 100%; object-fit: contain;
|
||||
}
|
||||
.hero-content { position: relative; z-index: 1; }
|
||||
@media (max-width: 600px) {
|
||||
.hero-blob-br { width: 200px; height: 240px; right: -55px; bottom: -65px; }
|
||||
.hero-blob-bl { width: 180px; height: 210px; left: -50px; bottom: -55px; }
|
||||
.hero-blob-tl { width: 140px; height: 160px; left: -40px; top: -10px; }
|
||||
}
|
||||
.hero-emoji { font-size: 4rem; margin-bottom: 1rem; display: block; }
|
||||
.hero-desc {
|
||||
font-size: clamp(0.9rem, 2vw, 1.05rem);
|
||||
@@ -118,6 +163,7 @@
|
||||
border-radius: 18px; padding: 1.75rem;
|
||||
border: 1px solid rgba(180,170,220,0.2);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
-webkit-transform: translateZ(0); transform: translateZ(0);
|
||||
}
|
||||
.testimonial-card:hover {
|
||||
transform: translateY(-3px);
|
||||
@@ -147,6 +193,7 @@
|
||||
border-radius: 18px; padding: 2rem 2.5rem;
|
||||
border: 1px solid rgba(180,170,220,0.2);
|
||||
text-align: center;
|
||||
-webkit-transform: translateZ(0); transform: translateZ(0);
|
||||
}
|
||||
.pricing-text {
|
||||
font-size: clamp(1.1rem, 2.5vw, 1.3rem);
|
||||
@@ -177,6 +224,7 @@
|
||||
border-radius: 18px; padding: 2rem 1.75rem;
|
||||
border: 1px solid rgba(180,170,220,0.2);
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
-webkit-transform: translateZ(0); transform: translateZ(0);
|
||||
}
|
||||
.service-card:hover {
|
||||
transform: translateY(-4px);
|
||||
@@ -210,6 +258,7 @@
|
||||
border-radius: 22px; padding: 2.5rem 2rem;
|
||||
box-shadow: 0 8px 40px rgba(124,108,255,0.12);
|
||||
border: 1px solid rgba(180,170,220,0.25);
|
||||
-webkit-transform: translateZ(0); transform: translateZ(0);
|
||||
}
|
||||
.form-wrapper h2 {
|
||||
text-align: center; font-size: 1.6rem; font-weight: 800;
|
||||
@@ -286,11 +335,29 @@
|
||||
|
||||
<!-- Hero -->
|
||||
<section class="hero">
|
||||
<span class="hero-emoji" role="img" aria-label="pets">🐱🐹🦎</span>
|
||||
<h1>{{ t.landing_hero_title }}</h1>
|
||||
<p>{{ t.landing_hero_subtitle }}</p>
|
||||
<p class="hero-desc">{{ t.landing_hero_description }}</p>
|
||||
<a href="#form" class="hero-cta">{{ t.landing_hero_cta }}</a>
|
||||
<div class="hero-blob hero-blob-tl">
|
||||
<svg viewBox="0 0 60 70" width="50" height="58" style="left:15%;top:18%;transform:rotate(-15deg);"><ellipse cx="30" cy="46" rx="16" ry="18" fill="#8cb8e8" opacity="0.55"/><ellipse cx="14" cy="22" rx="8" ry="10" fill="#8cb8e8" opacity="0.55" transform="rotate(-10 14 22)"/><ellipse cx="46" cy="22" rx="8" ry="10" fill="#8cb8e8" opacity="0.55" transform="rotate(10 46 22)"/><ellipse cx="24" cy="8" rx="6" ry="8" fill="#8cb8e8" opacity="0.55" transform="rotate(-5 24 8)"/><ellipse cx="38" cy="8" rx="6" ry="8" fill="#8cb8e8" opacity="0.55" transform="rotate(5 38 8)"/></svg>
|
||||
<svg viewBox="0 0 60 55" width="32" height="30" style="right:20%;bottom:22%;transform:rotate(12deg);"><path d="M30 50 C10 35 0 22 0 14 C0 5 7 0 15 0 C21 0 26 3 30 9 C34 3 39 0 45 0 C53 0 60 5 60 14 C60 22 50 35 30 50Z" fill="#7baad4" opacity="0.5"/></svg>
|
||||
<svg viewBox="0 0 80 40" width="44" height="22" style="left:50%;top:60%;transform:rotate(8deg);"><path d="M60 20 Q48 2 28 8 Q10 14 10 20 Q10 26 28 32 Q48 38 60 20Z" fill="#9ac4e8" opacity="0.3"/><path d="M60 20 L74 6 L74 34 Z" fill="#9ac4e8" opacity="0.3"/></svg>
|
||||
</div>
|
||||
<div class="hero-blob hero-blob-bl">
|
||||
<svg viewBox="0 0 60 70" width="45" height="52" style="right:18%;top:15%;transform:rotate(20deg);"><path d="M30 65 C30 65 10 45 10 25 C10 10 25 2 30 2 C35 2 50 10 50 25 C50 45 30 65 30 65Z" fill="#8cc49a" opacity="0.35"/><path d="M30 60 L30 20 M30 35 Q20 30 15 25 M30 45 Q40 40 45 35" fill="none" stroke="#7ab88a" stroke-width="2" stroke-linecap="round"/></svg>
|
||||
<svg viewBox="0 0 80 40" width="40" height="20" style="left:30%;bottom:25%;transform:rotate(-12deg);"><path d="M60 20 Q48 2 28 8 Q10 14 10 20 Q10 26 28 32 Q48 38 60 20Z" fill="#90c8a0" opacity="0.3"/><path d="M60 20 L74 6 L74 34 Z" fill="#90c8a0" opacity="0.3"/></svg>
|
||||
<svg viewBox="0 0 60 55" width="30" height="28" style="right:30%;bottom:40%;transform:rotate(15deg);"><path d="M30 50 C10 35 0 22 0 14 C0 5 7 0 15 0 C21 0 26 3 30 9 C34 3 39 0 45 0 C53 0 60 5 60 14 C60 22 50 35 30 50Z" fill="#7cb88c" opacity="0.5"/></svg>
|
||||
</div>
|
||||
<div class="hero-blob hero-blob-br">
|
||||
<svg viewBox="0 0 80 80" width="60" height="60" style="left:12%;top:15%;transform:rotate(10deg);"><path d="M20 15 L10 2 L18 18 M60 15 L70 2 L62 18 M40 70 C18 70 8 52 8 38 C8 20 22 8 40 8 C58 8 72 20 72 38 C72 52 62 70 40 70Z" fill="none" stroke="#e89cb8" stroke-width="3" stroke-linecap="round"/><circle cx="28" cy="36" r="3.5" fill="#e89cb8"/><circle cx="52" cy="36" r="3.5" fill="#e89cb8"/><ellipse cx="40" cy="48" rx="4" ry="2.5" fill="#e89cb8"/></svg>
|
||||
<svg viewBox="0 0 60 70" width="40" height="47" style="right:15%;top:50%;transform:rotate(-20deg);"><ellipse cx="30" cy="46" rx="16" ry="18" fill="#e8a0b8" opacity="0.55"/><ellipse cx="14" cy="22" rx="8" ry="10" fill="#e8a0b8" opacity="0.55" transform="rotate(-10 14 22)"/><ellipse cx="46" cy="22" rx="8" ry="10" fill="#e8a0b8" opacity="0.55" transform="rotate(10 46 22)"/><ellipse cx="24" cy="8" rx="6" ry="8" fill="#e8a0b8" opacity="0.55" transform="rotate(-5 24 8)"/><ellipse cx="38" cy="8" rx="6" ry="8" fill="#e8a0b8" opacity="0.55" transform="rotate(5 38 8)"/></svg>
|
||||
<svg viewBox="0 0 60 55" width="36" height="33" style="left:55%;bottom:18%;transform:rotate(-8deg);"><path d="M30 50 C10 35 0 22 0 14 C0 5 7 0 15 0 C21 0 26 3 30 9 C34 3 39 0 45 0 C53 0 60 5 60 14 C60 22 50 35 30 50Z" fill="#d48aaa" opacity="0.5"/></svg>
|
||||
<svg viewBox="0 0 60 70" width="30" height="35" style="left:20%;bottom:30%;transform:rotate(25deg);"><path d="M30 65 C30 65 10 45 10 25 C10 10 25 2 30 2 C35 2 50 10 50 25 C50 45 30 65 30 65Z" fill="#e8b0c4" opacity="0.35"/><path d="M30 60 L30 20 M30 35 Q20 30 15 25 M30 45 Q40 40 45 35" fill="none" stroke="#d49ab0" stroke-width="2" stroke-linecap="round"/></svg>
|
||||
</div>
|
||||
<div class="hero-content">
|
||||
<span class="hero-emoji" role="img" aria-label="pets">🐱🐹🦎</span>
|
||||
<h1>{{ t.landing_hero_title }}</h1>
|
||||
<p>{{ t.landing_hero_subtitle }}</p>
|
||||
<p class="hero-desc">{{ t.landing_hero_description }}</p>
|
||||
<a href="#form" class="hero-cta">{{ t.landing_hero_cta }}</a>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Services -->
|
||||
|
||||
Reference in New Issue
Block a user