1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct AdminConfig {
8 pub title: String,
10 pub base_path: String,
12 pub theme: Theme,
14 pub items_per_page: usize,
16 pub max_items_per_page: usize,
18 pub require_auth: bool,
20 pub enable_search: bool,
22 pub enable_export: bool,
24 pub date_format: String,
26 pub datetime_format: String,
28 pub logo_url: Option<String>,
30 pub favicon_url: Option<String>,
32 pub custom_css: Option<String>,
34 pub custom_js: Option<String>,
36 pub footer_text: Option<String>,
38}
39
40impl AdminConfig {
41 pub fn format_date(&self, raw: &str) -> String {
46 chrono::NaiveDate::parse_from_str(raw, "%Y-%m-%d")
47 .map(|d| d.format(&self.date_format).to_string())
48 .unwrap_or_else(|_| raw.to_string())
49 }
50
51 pub fn format_datetime(&self, raw: &str) -> String {
56 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(raw) {
57 return dt.format(&self.datetime_format).to_string();
58 }
59 if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(raw, "%Y-%m-%dT%H:%M:%S") {
60 return dt.format(&self.datetime_format).to_string();
61 }
62 raw.to_string()
63 }
64}
65
66impl Default for AdminConfig {
67 fn default() -> Self {
68 Self {
69 title: "Admin Dashboard".to_string(),
70 base_path: "/admin".to_string(),
71 theme: Theme::default(),
72 items_per_page: 25,
73 max_items_per_page: 100,
74 require_auth: true,
75 enable_search: true,
76 enable_export: true,
77 date_format: "%Y-%m-%d".to_string(),
78 datetime_format: "%Y-%m-%d %H:%M:%S".to_string(),
79 logo_url: None,
80 favicon_url: None,
81 custom_css: None,
82 custom_js: None,
83 footer_text: None,
84 }
85 }
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct Theme {
91 pub name: ThemePreset,
93 pub primary_color: String,
95 pub secondary_color: String,
97 pub accent_color: String,
99 pub background_color: String,
101 pub surface_color: String,
103 pub text_color: String,
105 pub text_muted_color: String,
107 pub border_color: String,
109 pub success_color: String,
111 pub warning_color: String,
113 pub error_color: String,
115 pub sidebar_width: String,
117 pub border_radius: String,
119 pub font_family: String,
121}
122
123impl Default for Theme {
124 fn default() -> Self {
125 Self::dark()
126 }
127}
128
129impl Theme {
130 pub fn dark() -> Self {
132 Self {
133 name: ThemePreset::Dark,
134 primary_color: "#6366f1".to_string(), secondary_color: "#8b5cf6".to_string(), accent_color: "#22d3ee".to_string(), background_color: "#0f172a".to_string(), surface_color: "#1e293b".to_string(), text_color: "#f8fafc".to_string(), text_muted_color: "#94a3b8".to_string(), border_color: "#334155".to_string(), success_color: "#22c55e".to_string(), warning_color: "#f59e0b".to_string(), error_color: "#ef4444".to_string(), sidebar_width: "260px".to_string(),
146 border_radius: "0.5rem".to_string(),
147 font_family: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
148 .to_string(),
149 }
150 }
151
152 pub fn light() -> Self {
154 Self {
155 name: ThemePreset::Light,
156 primary_color: "#4f46e5".to_string(), secondary_color: "#7c3aed".to_string(), accent_color: "#0891b2".to_string(), background_color: "#f8fafc".to_string(), surface_color: "#ffffff".to_string(), text_color: "#0f172a".to_string(), text_muted_color: "#64748b".to_string(), border_color: "#e2e8f0".to_string(), success_color: "#16a34a".to_string(), warning_color: "#d97706".to_string(), error_color: "#dc2626".to_string(), sidebar_width: "260px".to_string(),
168 border_radius: "0.5rem".to_string(),
169 font_family: "'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
170 .to_string(),
171 }
172 }
173
174 pub fn corporate() -> Self {
176 Self {
177 name: ThemePreset::Corporate,
178 primary_color: "#2563eb".to_string(), secondary_color: "#1d4ed8".to_string(), accent_color: "#0ea5e9".to_string(), background_color: "#f1f5f9".to_string(), surface_color: "#ffffff".to_string(),
183 text_color: "#1e293b".to_string(), text_muted_color: "#64748b".to_string(), border_color: "#cbd5e1".to_string(), success_color: "#059669".to_string(), warning_color: "#ca8a04".to_string(), error_color: "#dc2626".to_string(), sidebar_width: "240px".to_string(),
190 border_radius: "0.375rem".to_string(),
191 font_family:
192 "'IBM Plex Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
193 .to_string(),
194 }
195 }
196
197 pub fn to_css_variables(&self) -> String {
199 format!(
200 r#":root {{
201 --admin-primary: {};
202 --admin-secondary: {};
203 --admin-accent: {};
204 --admin-bg: {};
205 --admin-surface: {};
206 --admin-text: {};
207 --admin-text-muted: {};
208 --admin-border: {};
209 --admin-success: {};
210 --admin-warning: {};
211 --admin-error: {};
212 --admin-sidebar-width: {};
213 --admin-radius: {};
214 --admin-font: {};
215}}"#,
216 self.primary_color,
217 self.secondary_color,
218 self.accent_color,
219 self.background_color,
220 self.surface_color,
221 self.text_color,
222 self.text_muted_color,
223 self.border_color,
224 self.success_color,
225 self.warning_color,
226 self.error_color,
227 self.sidebar_width,
228 self.border_radius,
229 self.font_family,
230 )
231 }
232}
233
234#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
236pub enum ThemePreset {
237 #[default]
238 Dark,
239 Light,
240 Corporate,
241 Custom,
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn test_default_config() {
250 let config = AdminConfig::default();
251 assert_eq!(config.title, "Admin Dashboard");
252 assert_eq!(config.base_path, "/admin");
253 assert_eq!(config.items_per_page, 25);
254 }
255
256 #[test]
257 fn test_theme_presets() {
258 let dark = Theme::dark();
259 assert_eq!(dark.name, ThemePreset::Dark);
260
261 let light = Theme::light();
262 assert_eq!(light.name, ThemePreset::Light);
263 }
264
265 #[test]
266 fn test_css_variables() {
267 let theme = Theme::dark();
268 let css = theme.to_css_variables();
269 assert!(css.contains("--admin-primary:"));
270 assert!(css.contains("--admin-bg:"));
271 }
272}