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