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        && 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
42// 3. Fungsi Helper untuk Render HTML Statis
43pub 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    // Register custom filters
56    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
141// 4. Fungsi Helper untuk Render dengan Session
142pub 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    // Default keys for flash messages
152    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            // Register custom filters
192            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}