Added reviews. Added pricing.
Build and Publish / Build and Publish Docker Image (push) Successful in 6m27s

This commit is contained in:
Ultradesu
2026-05-11 11:34:11 +01:00
parent ff32e6bbaf
commit d1ef66acc1
14 changed files with 839 additions and 194 deletions
+76 -8
View File
@@ -1,16 +1,16 @@
use cot::Template;
use cot::db::{Auto, Database, Model};
use cot::html::Html;
use cot::request::Request;
use cot::request::extractors::Path;
use cot::response::{IntoResponse, Redirect, Response};
use cot::router::{Route, Router};
use cot::Template;
use serde::Deserialize;
use cot::db::query;
use crate::i18n::{Lang, Translations};
use crate::models::{Client, Lead, Media, Setting, User, Visit};
use crate::models::{Client, Lead, Media, Setting, Testimonial, User, Visit};
use crate::telegram;
fn detect_lang(request: &Request) -> Lang {
@@ -70,6 +70,8 @@ struct LandingTemplate<'a> {
t: &'a Translations,
lang: Lang,
contact_info: String,
pricing_info: String,
testimonials: Vec<Testimonial>,
}
#[derive(Debug, Template)]
@@ -87,7 +89,23 @@ async fn landing_page(request: Request, db: Database) -> cot::Result<Response> {
.await?
.map(|s| s.value)
.unwrap_or_default();
let body = LandingTemplate { t: lang.t(), lang, contact_info }.render()?;
let pricing_key = "pricing_info".to_string();
let pricing_info = query!(Setting, $key == pricing_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));
let body = LandingTemplate {
t: lang.t(),
lang,
contact_info,
pricing_info,
testimonials,
}
.render()?;
html_response(body, lang)
}
@@ -173,10 +191,12 @@ async fn client_portal(
let today = chrono::Utc::now().date_naive();
let mut visits = Visit::objects().all(&db).await?;
visits.retain(|v| {
v.client_id.primary_key().unwrap() == client_id && v.status != "cancelled"
visits.retain(|v| v.client_id.primary_key().unwrap() == client_id && v.status != "cancelled");
visits.sort_by(|a, b| {
a.visit_date
.cmp(&b.visit_date)
.then(a.time_start.cmp(&b.time_start))
});
visits.sort_by(|a, b| a.visit_date.cmp(&b.visit_date).then(a.time_start.cmp(&b.time_start)));
let users = User::objects().all(&db).await?;
let all_media = Media::objects().all(&db).await?;
@@ -200,7 +220,11 @@ async fn client_portal(
})
.cloned()
.collect();
PortalVisit { visit: v, admin_name, media }
PortalVisit {
visit: v,
admin_name,
media,
}
};
let mut upcoming = Vec::new();
@@ -258,7 +282,12 @@ async fn submit_feedback(
}
}
Redirect::new(format!("/client/{}?lang={}&feedback=ok", token_clone, lang.code())).into_response()
Redirect::new(format!(
"/client/{}?lang={}&feedback=ok",
token_clone,
lang.code()
))
.into_response()
}
/// Serve media files for the client portal (no auth required, but only via token).
@@ -303,10 +332,49 @@ async fn portal_media(
}
}
async fn serve_testimonial_image(
_request: Request,
db: Database,
Path(id): Path<i64>,
) -> cot::Result<Response> {
let testimonial = match query!(Testimonial, $id == id).get(&db).await? {
Some(t) => t,
None => return Html::new("404").into_response(),
};
let path = match &testimonial.image_path {
Some(p) => p.clone(),
None => return Html::new("404").into_response(),
};
match tokio::fs::read(&path).await {
Ok(data) => {
let content_type = match path.rsplit('.').next().unwrap_or("") {
"jpg" | "jpeg" => "image/jpeg",
"png" => "image/png",
"webp" => "image/webp",
"heic" | "heif" => "image/heic",
_ => "application/octet-stream",
};
let body = cot::Body::fixed(data);
let mut resp = Response::new(body);
resp.headers_mut()
.insert("content-type", content_type.parse().unwrap());
resp.headers_mut()
.insert("cache-control", "public, max-age=86400".parse().unwrap());
Ok(resp)
}
Err(_) => Html::new("404").into_response(),
}
}
pub fn public_router() -> Router {
Router::with_urls([
Route::with_handler_and_name("/", landing_page, "landing"),
Route::with_handler_and_name("/submit", submit_lead, "submit-lead"),
Route::with_handler_and_name(
"/testimonial-image/{id}",
serve_testimonial_image,
"testimonial-image",
),
Route::with_handler_and_name("/client/{token}", client_portal, "client-portal"),
Route::with_handler_and_name(
"/client/{token}/{visit_id}/feedback",