1use crate::router::{Html, IntoResponse, Response};
7use http::StatusCode;
8use crate::chrono::{DateTime, FixedOffset};
9use crate::chrono_tz::{self, Tz};
10use crate::requests::Request as AppRequest;
11use crate::Config;
12use serde_json::{json, Value};
13use crate::template::TemplateEngine;
14
15use crate::tracing;
16
17static EMBEDDED_TEMPLATES_GET: std::sync::OnceLock<fn(&str) -> Option<crate::rust_embed::EmbeddedFile>> = std::sync::OnceLock::new();
18
19pub fn set_embedded_templates(f: fn(&str) -> Option<crate::rust_embed::EmbeddedFile>) {
20 EMBEDDED_TEMPLATES_GET.set(f).ok();
21}
22
23fn load_template_content(name: &str) -> Result<String, String> {
24 let cfg = Config::load();
25 if cfg.app_debug {
26 let path = format!("src/resources/views/{}", name);
27 if let Ok(content) = std::fs::read_to_string(&path) {
28 return Ok(content);
29 }
30 }
31
32 let file = EMBEDDED_TEMPLATES_GET.get().and_then(|f| f(name));
34 if let Some(file) = file
35 && let Ok(content) = std::str::from_utf8(&file.data) {
36 return Ok(content.to_string());
37 }
38
39 Err(format!("Template '{}' tidak ditemukan", name))
40}
41
42pub fn render(template: &str, context: Value) -> Response {
44 render_internal(template, context)
45}
46
47pub fn render_to_string(template: &str, context: Value) -> String {
48 let content = match load_template_content(template) {
49 Ok(c) => c,
50 Err(e) => return format!("Template error: {}", e),
51 };
52
53 let mut engine = TemplateEngine::new();
54
55 engine.add_filter("diff_for_humans", |val: &Value, _args: &[Value]| {
57 if let Some(value) = val.as_str()
58 && let Ok(dt) = DateTime::<FixedOffset>::parse_from_rfc3339(value) {
59 let now = crate::chrono::Utc::now();
60 let dt_utc = dt.with_timezone(&crate::chrono::Utc);
61 let duration = now.signed_duration_since(dt_utc);
62 let seconds = duration.num_seconds();
63 let result = if seconds < 0 {
64 let seconds = -seconds;
65 if seconds < 60 {
66 "in a few seconds".to_string()
67 } else {
68 let minutes = seconds / 60;
69 if minutes < 60 {
70 format!("in {} minute{}", minutes, if minutes > 1 { "s" } else { "" })
71 } else {
72 let hours = minutes / 60;
73 if hours < 24 {
74 format!("in {} hour{}", hours, if hours > 1 { "s" } else { "" })
75 } else {
76 let days = hours / 24;
77 if days < 30 {
78 format!("in {} day{}", days, if days > 1 { "s" } else { "" })
79 } else {
80 let months = days / 30;
81 if months < 12 {
82 format!("in {} month{}", months, if months > 1 { "s" } else { "" })
83 } else {
84 let years = months / 12;
85 format!("in {} year{}", years, if years > 1 { "s" } else { "" })
86 }
87 }
88 }
89 }
90 }
91 } else {
92 if seconds < 60 {
93 "a few seconds ago".to_string()
94 } else {
95 let minutes = seconds / 60;
96 if minutes < 60 {
97 format!("{} minute{} ago", minutes, if minutes > 1 { "s" } else { "" })
98 } else {
99 let hours = minutes / 60;
100 if hours < 24 {
101 format!("{} hour{} ago", hours, if hours > 1 { "s" } else { "" })
102 } else {
103 let days = hours / 24;
104 if days < 30 {
105 format!("{} day{} ago", days, if days > 1 { "s" } else { "" })
106 } else {
107 let months = days / 30;
108 if months < 12 {
109 format!("{} month{} ago", months, if months > 1 { "s" } else { "" })
110 } else {
111 let years = months / 12;
112 format!("{} year{} ago", years, if years > 1 { "s" } else { "" })
113 }
114 }
115 }
116 }
117 }
118 };
119 return Value::String(result);
120 }
121 val.clone()
122 });
123
124 engine.add_filter("format_date", |val: &Value, args: &[Value]| {
125 let fmt = args.first().and_then(|a| a.as_str()).unwrap_or("%Y-%m-%d");
126 if let Some(value) = val.as_str() {
127 let cfg = Config::load();
128 let tz_str = cfg.app_timezone.trim();
129 let tz: Tz = tz_str.parse().unwrap_or(chrono_tz::UTC);
130
131 if let Ok(dt) = DateTime::<FixedOffset>::parse_from_rfc3339(value) {
132 return Value::String(dt.with_timezone(&tz).format(fmt).to_string());
133 }
134 }
135 val.clone()
136 });
137
138 engine.render(&content, &context).unwrap_or_else(|e| format!("Render error: {}", e))
139}
140
141pub fn view(req: &AppRequest, template: &str, ctx: Value) -> Response {
143 let mut ctx_value = ctx;
144
145 if !ctx_value.is_object() {
146 ctx_value = json!({});
147 }
148
149 let obj = ctx_value.as_object_mut().unwrap();
150
151 if !obj.contains_key("errors") { obj.insert("errors".to_string(), json!({})); }
153 if !obj.contains_key("old") { obj.insert("old".to_string(), json!({})); }
154 if !obj.contains_key("flash_success") { obj.insert("flash_success".to_string(), json!("")); }
155 if !obj.contains_key("flash_error") { obj.insert("flash_error".to_string(), json!("")); }
156
157 if let Some(success) = req.session.get::<String>("flash_success") {
158 obj.insert("flash_success".to_string(), json!(success));
159 req.session.remove("flash_success");
160 }
161 if let Some(error) = req.session.get::<String>("flash_error") {
162 obj.insert("flash_error".to_string(), json!(error));
163 req.session.remove("flash_error");
164 }
165 if let Some(errors) = req.session.get::<Value>("errors") {
166 obj.insert("errors".to_string(), errors);
167 req.session.remove("errors");
168 }
169 if let Some(old) = req.session.get::<Value>("old_input") {
170 obj.insert("old".to_string(), old);
171 }
172
173 if let Some(token) = req.session.get::<String>("_token") {
174 obj.insert("csrf_token".to_string(), json!(token));
175 }
176
177 let is_logged_in = req.session.get::<i64>("user_id").is_some();
178 obj.insert("auth".to_string(), json!(is_logged_in));
179
180 render_internal(template, ctx_value)
181}
182
183fn render_internal(template: &str, context: Value) -> Response {
184 let cfg = crate::Config::load();
185 tracing::debug!("Rendering template: {} (APP_DEBUG: {})", template, cfg.app_debug);
186
187 match load_template_content(template) {
188 Ok(content) => {
189 let mut engine = TemplateEngine::new();
190
191 engine.add_filter("diff_for_humans", |val: &Value, _args: &[Value]| {
193 if let Some(value) = val.as_str()
194 && let Ok(dt) = DateTime::<FixedOffset>::parse_from_rfc3339(value) {
195 let now = crate::chrono::Utc::now();
196 let dt_utc = dt.with_timezone(&crate::chrono::Utc);
197 let duration = now.signed_duration_since(dt_utc);
198 let seconds = duration.num_seconds();
199 let result = if seconds < 0 {
200 let seconds = -seconds;
201 if seconds < 60 {
202 "in a few seconds".to_string()
203 } else {
204 let minutes = seconds / 60;
205 if minutes < 60 {
206 format!("in {} minute{}", minutes, if minutes > 1 { "s" } else { "" })
207 } else {
208 let hours = minutes / 60;
209 if hours < 24 {
210 format!("in {} hour{}", hours, if hours > 1 { "s" } else { "" })
211 } else {
212 let days = hours / 24;
213 if days < 30 {
214 format!("in {} day{}", days, if days > 1 { "s" } else { "" })
215 } else {
216 let months = days / 30;
217 if months < 12 {
218 format!("in {} month{}", months, if months > 1 { "s" } else { "" })
219 } else {
220 let years = months / 12;
221 format!("in {} year{}", years, if years > 1 { "s" } else { "" })
222 }
223 }
224 }
225 }
226 }
227 } else {
228 if seconds < 60 {
229 "a few seconds ago".to_string()
230 } else {
231 let minutes = seconds / 60;
232 if minutes < 60 {
233 format!("{} minute{} ago", minutes, if minutes > 1 { "s" } else { "" })
234 } else {
235 let hours = minutes / 60;
236 if hours < 24 {
237 format!("{} hour{} ago", hours, if hours > 1 { "s" } else { "" })
238 } else {
239 let days = hours / 24;
240 if days < 30 {
241 format!("{} day{} ago", days, if days > 1 { "s" } else { "" })
242 } else {
243 let months = days / 30;
244 if months < 12 {
245 format!("{} month{} ago", months, if months > 1 { "s" } else { "" })
246 } else {
247 let years = months / 12;
248 format!("{} year{} ago", years, if years > 1 { "s" } else { "" })
249 }
250 }
251 }
252 }
253 }
254 };
255 return Value::String(result);
256 }
257 val.clone()
258 });
259
260 engine.add_filter("format_date", |val: &Value, args: &[Value]| {
261 let fmt = args.first().and_then(|a| a.as_str()).unwrap_or("%Y-%m-%d");
262 if let Some(value) = val.as_str() {
263 let cfg = Config::load();
264 let tz_str = cfg.app_timezone.trim();
265 let tz: Tz = tz_str.parse().unwrap_or(chrono_tz::UTC);
266
267 if let Ok(dt) = DateTime::<FixedOffset>::parse_from_rfc3339(value) {
268 return Value::String(dt.with_timezone(&tz).format(fmt).to_string());
269 }
270 }
271 val.clone()
272 });
273
274 match engine.render(&content, &context) {
275 Ok(rendered) => Html(rendered).into_response(),
276 Err(err) => {
277 tracing::error!("Gagal render template: {}", err);
278 if cfg.app_debug {
279 return (StatusCode::INTERNAL_SERVER_ERROR, format!("Render Error: {}", err)).into_response();
280 }
281 (StatusCode::INTERNAL_SERVER_ERROR, "Internal Server Error").into_response()
282 }
283 }
284 }
285 Err(err) => {
286 tracing::error!("Template tidak ditemukan: {}", err);
287 if cfg.app_debug {
288 return (StatusCode::NOT_FOUND, format!("Template Not Found: {}", err)).into_response();
289 }
290 (StatusCode::NOT_FOUND, "Not Found").into_response()
291 }
292 }
293}