diff --git a/src/handlers.rs b/src/handlers.rs new file mode 100644 index 0000000..e6256d7 --- /dev/null +++ b/src/handlers.rs @@ -0,0 +1,65 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::Json, +}; +use uuid::Uuid; +use chrono::Utc; +use crate::model::{Entry, AppState}; + +pub async fn list_entries(State(state): State) -> Json> { + let entries = state.entries.lock().unwrap(); + let mut sorted = entries.clone(); + // Sort: Pinned/Daily first, then by date + sorted.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + Json(sorted) +} + +pub async fn create_entry( + State(state): State, + Json(mut payload): Json, +) -> Json { + if payload.id == Uuid::nil() { payload.id = Uuid::new_v4(); } + payload.created_at = Utc::now(); + payload.status = "Active".to_string(); + + { + let mut entries = state.entries.lock().unwrap(); + entries.push(payload.clone()); + } + state.save(); + Json(payload) +} + +pub async fn update_entry( + Path(id): Path, + State(state): State, + Json(payload): Json, +) -> StatusCode { + let mut entries = state.entries.lock().unwrap(); + if let Some(entry) = entries.iter_mut().find(|e| e.id == id) { + entry.title = payload.title; + entry.description = payload.description; + entry.tags = payload.tags; + entry.frequency = payload.frequency; + entry.details = payload.details; + entry.event_date = payload.event_date; + entry.updated_at = Some(Utc::now()); + drop(entries); + state.save(); + StatusCode::OK + } else { + StatusCode::NOT_FOUND + } +} + +pub async fn delete_entry( + Path(id): Path, + State(state): State, +) -> StatusCode { + let mut entries = state.entries.lock().unwrap(); + entries.retain(|e| e.id != id); + drop(entries); + state.save(); + StatusCode::NO_CONTENT +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f294ae9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,30 @@ +mod handlers; +mod model; + +use axum::{ + routing::{get, put, delete}, // post is implied in route chaining + Router, +}; +use std::net::SocketAddr; +use tower_http::{services::ServeDir, trace::TraceLayer}; +use model::AppState; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + let state = AppState::new("data.json"); + + let app = Router::new() + .route("/api/entries", get(handlers::list_entries).post(handlers::create_entry)) + .route("/api/entries/:id", put(handlers::update_entry).delete(handlers::delete_entry)) + .nest_service("/", ServeDir::new("static")) + .with_state(state) + .layer(TraceLayer::new_for_http()); + + let addr = SocketAddr::from(([127, 0, 0, 1], 3030)); + println!("Life Manager running at http://{}", addr); + + let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); + axum::serve(listener, app).await.unwrap(); +} diff --git a/src/model.rs b/src/model.rs new file mode 100644 index 0000000..df4c8ea --- /dev/null +++ b/src/model.rs @@ -0,0 +1,71 @@ +use serde::{Deserialize, Serialize}; +use std::sync::{Arc, Mutex}; +use uuid::Uuid; +use serde_json::Value; +use chrono::{DateTime, Utc}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Entry { + #[serde(default = "Uuid::new_v4")] + pub id: Uuid, + pub template_type: String, + pub title: String, + + #[serde(default)] + pub description: String, + + #[serde(default)] + pub tags: Vec, + + pub frequency: Option, + + #[serde(default = "default_status")] + pub status: String, + + // NEW FIELD: Holds specific appointment times + #[serde(default)] + pub event_date: Option>, + + pub details: Value, + + #[serde(default = "Utc::now")] + pub created_at: DateTime, + + pub updated_at: Option>, +} + +// Helper function for the default value +fn default_status() -> String { + "Active".to_string() +} + +#[derive(Clone)] +pub struct AppState { + pub entries: Arc>>, + pub file_path: String, +} + +impl AppState { + pub fn new(file_path: &str) -> Self { + // Try to read file, otherwise create empty list + let entries = match std::fs::read_to_string(file_path) { + Ok(content) => serde_json::from_str(&content).unwrap_or_default(), + Err(_) => Vec::new(), + }; + Self { + entries: Arc::new(Mutex::new(entries)), + file_path: file_path.to_string(), + } + } + + pub fn save(&self) { + let entries = self.entries.lock().unwrap(); + let content = serde_json::to_string_pretty(&*entries).unwrap(); + // Added error printing so you can see if file permissions are wrong + if let Err(e) = std::fs::write(&self.file_path, content) { + eprintln!("CRITICAL ERROR: Could not save data.json: {}", e); + } else { + println!("Database saved to {}", self.file_path); + } + } +}