use serde::{Deserialize, Serialize}; // ─── Domain types ───────────────────────────────────────────────────────────── #[derive(Debug, Clone, Deserialize)] pub struct Space { pub id: String, pub name: String, pub icon: Option, } #[derive(Debug, Clone, Deserialize)] pub struct Page { pub id: String, pub title: Option, pub icon: Option, } #[derive(Deserialize)] pub struct DataWrapper { pub items: Vec, } #[derive(Deserialize)] #[serde(untagged)] pub enum ApiList { Wrapped { data: DataWrapper }, Paginated { items: Vec }, Plain(Vec), } impl ApiList { pub fn into_items(self) -> Vec { match self { ApiList::Wrapped { data } => data.items, ApiList::Paginated { items } => items, ApiList::Plain(v) => v, } } } // ─── App state ──────────────────────────────────────────────────────────────── #[derive(Debug, Clone, Copy, PartialEq)] pub enum AppState { Login, Main, Editor, Search, } // ─── Login form ─────────────────────────────────────────────────────────────── #[derive(Debug, Clone, Copy, PartialEq)] pub enum LoginField { Url, Email, Password, } impl LoginField { pub fn next(self) -> Self { match self { LoginField::Url => LoginField::Email, LoginField::Email => LoginField::Password, LoginField::Password => LoginField::Url, } } pub fn prev(self) -> Self { match self { LoginField::Url => LoginField::Password, LoginField::Email => LoginField::Url, LoginField::Password => LoginField::Email, } } } pub struct LoginForm { pub url: String, pub email: String, pub password: String, pub active_field: LoginField, pub error: Option, pub submitting: bool, } impl LoginForm { pub fn new() -> Self { Self { url: std::env::var("DOCMOST_URL") .unwrap_or_else(|_| "http://localhost:3000".to_string()), email: std::env::var("DOCMOST_EMAIL").unwrap_or_default(), password: String::new(), active_field: if std::env::var("DOCMOST_EMAIL").is_ok() { LoginField::Password } else { LoginField::Email }, error: None, submitting: false, } } pub fn active_field_value_mut(&mut self) -> &mut String { match self.active_field { LoginField::Url => &mut self.url, LoginField::Email => &mut self.email, LoginField::Password => &mut self.password, } } } // ─── Main view ──────────────────────────────────────────────────────────────── #[derive(Debug, Clone, Copy, PartialEq)] pub enum Panel { Spaces, Pages, } pub struct NewPageDialog { pub title: String, pub creating: bool, pub error: Option, } impl NewPageDialog { pub fn new() -> Self { Self { title: String::new(), creating: false, error: None } } } pub struct NewSpaceDialog { pub name: String, pub creating: bool, pub error: Option, } impl NewSpaceDialog { pub fn new() -> Self { Self { name: String::new(), creating: false, error: None } } } #[derive(Debug, Clone)] pub enum DeleteTarget { Space { id: String, name: String }, Page { id: String, title: String }, } impl DeleteTarget { pub fn display_name(&self) -> &str { match self { DeleteTarget::Space { name, .. } => name, DeleteTarget::Page { title, .. } => title, } } pub fn kind(&self) -> &str { match self { DeleteTarget::Space { .. } => "space", DeleteTarget::Page { .. } => "page", } } } pub struct DeleteConfirmDialog { pub target: DeleteTarget, pub deleting: bool, pub error: Option, } impl DeleteConfirmDialog { pub fn new(target: DeleteTarget) -> Self { Self { target, deleting: false, error: None } } } pub struct MainView { pub spaces: Vec, pub pages: Vec, pub selected_space: usize, pub selected_page: usize, pub focus: Panel, pub loading_spaces: bool, pub loading_pages: bool, pub loading_page_content: bool, pub error: Option, pub new_page_dialog: Option, pub new_space_dialog: Option, pub delete_dialog: Option, } impl MainView { pub fn new() -> Self { Self { spaces: vec![], pages: vec![], selected_space: 0, selected_page: 0, focus: Panel::Spaces, loading_spaces: true, loading_pages: false, loading_page_content: false, error: None, new_page_dialog: None, new_space_dialog: None, delete_dialog: None, } } } // ─── Editor view ───────────────────────────────────────────────────────────── #[derive(Debug, Clone, Copy, PartialEq)] pub enum EditorFocus { Title, Content, } pub struct EditorView { pub page_id: String, pub page_title: String, pub focus: EditorFocus, pub saving: bool, pub status: Option, } #[derive(Debug, Clone)] pub enum EditorStatus { Saved, Error(String), } impl EditorView { pub fn new(page_id: String, page_title: String) -> Self { Self { page_id, page_title, focus: EditorFocus::Content, saving: false, status: None, } } } // ─── Search view ───────────────────────────────────────────────────────────── #[derive(Debug, Clone, Deserialize)] pub struct SearchResultSpace { pub name: String, } #[derive(Debug, Clone, Deserialize)] pub struct SearchResult { pub id: String, pub title: Option, pub icon: Option, pub highlight: Option, pub space: Option, } pub struct SearchView { pub query: String, pub results: Vec, pub selected: usize, pub loading: bool, pub error: Option, pub opening_page: bool, } impl SearchView { pub fn new() -> Self { Self { query: String::new(), results: vec![], selected: 0, loading: false, error: None, opening_page: false, } } } // ─── Root app ───────────────────────────────────────────────────────────────── pub struct App { pub state: AppState, pub login: LoginForm, pub main: MainView, pub editor: EditorView, pub search: SearchView, pub base_url: String, pub token: String, } impl App { pub fn new() -> Self { Self { state: AppState::Login, login: LoginForm::new(), main: MainView::new(), editor: EditorView::new(String::new(), String::new()), search: SearchView::new(), base_url: String::new(), token: String::new(), } } } // ─── Messages ───────────────────────────────────────────────────────────────── pub enum AppMsg { LoginSuccess { token: String, base_url: String }, LoginError(String), SpacesLoaded(Vec), PagesLoaded(Vec), PageContentLoaded { page_id: String, title: String, content: String }, PageSaved, SaveError(String), PageCreated { page_id: String, title: String }, CreateError(String), SpaceCreated(Space), CreateSpaceError(String), SpaceDeleted(String), PageDeleted(String), DeleteError(String), SearchResults(Vec), SearchError(String), ApiError(String), } // ─── Request DTOs ───────────────────────────────────────────────────────────── #[derive(Serialize)] pub struct LoginRequest<'a> { pub email: &'a str, pub password: &'a str, } #[derive(Serialize)] pub struct SidebarPagesRequest<'a> { #[serde(rename = "spaceId")] pub space_id: &'a str, }