Skip to main content

rustbasic_core/
view.rs

1/* ---------------------------------------------------------
2 * 📑 LABEL: VIEW ENGINE (config/view.rs)
3 * Mengatur template engine (RustBasic Template) dan fungsi render.
4 * --------------------------------------------------------- */
5
6use 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    // Fallback ke embedded templates di memori
33    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
43// 3. Fungsi Helper untuk Render HTML Statis
44pub 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    // Register custom filters
57    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
143// 4. Fungsi Helper untuk Render dengan Session
144pub 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    // Default keys for flash messages
154    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            // Register custom filters
194            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}