This commit is contained in:
despiegk 2025-04-21 10:52:19 +02:00
parent 1fa0b30169
commit 4b637b7e04
14 changed files with 1023 additions and 5 deletions

View File

@ -144,7 +144,7 @@ dependencies = [
"futures-core", "futures-core",
"futures-util", "futures-util",
"mio", "mio",
"socket2", "socket2 0.5.9",
"tokio", "tokio",
"tracing", "tracing",
] ]
@ -223,7 +223,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"smallvec", "smallvec",
"socket2", "socket2 0.5.9",
"time", "time",
"tracing", "tracing",
"url", "url",
@ -258,6 +258,7 @@ dependencies = [
"lazy_static", "lazy_static",
"log", "log",
"num_cpus", "num_cpus",
"redis",
"serde", "serde",
"serde_json", "serde_json",
"tera", "tera",
@ -652,6 +653,20 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "combine"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
"bytes",
"futures-core",
"memchr",
"pin-project-lite",
"tokio",
"tokio-util",
]
[[package]] [[package]]
name = "config" name = "config"
version = "0.14.1" version = "0.14.1"
@ -1941,6 +1956,27 @@ dependencies = [
"getrandom 0.3.2", "getrandom 0.3.2",
] ]
[[package]]
name = "redis"
version = "0.23.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f49cdc0bb3f412bf8e7d1bd90fe1d9eb10bc5c399ba90973c14662a27b3f8ba"
dependencies = [
"async-trait",
"bytes",
"combine",
"futures-util",
"itoa",
"percent-encoding",
"pin-project-lite",
"ryu",
"sha1_smol",
"socket2 0.4.10",
"tokio",
"tokio-util",
"url",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.5.11" version = "0.5.11"
@ -2119,6 +2155,12 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "sha1_smol"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d"
[[package]] [[package]]
name = "sha2" name = "sha2"
version = "0.10.8" version = "0.10.8"
@ -2176,6 +2218,16 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "socket2"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
dependencies = [
"libc",
"winapi",
]
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.5.9" version = "0.5.9"
@ -2325,7 +2377,7 @@ dependencies = [
"parking_lot", "parking_lot",
"pin-project-lite", "pin-project-lite",
"signal-hook-registry", "signal-hook-registry",
"socket2", "socket2 0.5.9",
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
@ -2638,6 +2690,22 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]] [[package]]
name = "winapi-util" name = "winapi-util"
version = "0.1.9" version = "0.1.9"
@ -2647,6 +2715,12 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.61.0" version = "0.61.0"

View File

@ -21,3 +21,4 @@ actix-identity = "0.6.0"
bcrypt = "0.15.0" bcrypt = "0.15.0"
uuid = { version = "1.6.1", features = ["v4", "serde"] } uuid = { version = "1.6.1", features = ["v4", "serde"] }
lazy_static = "1.4.0" lazy_static = "1.4.0"
redis = { version = "0.23.0", features = ["tokio-comp"] }

56
actix_mvc_app/specs.md Normal file
View File

@ -0,0 +1,56 @@
- login
- change profile (email(s), linkedin, websites, telnr's, ...)
- KYC
- wallet
- with EUR/CHF/USD, ability to topup wallet... (stripe, make sure to describe well)
- with real world digital assets (RWDA)
- the RWDA can be transfered to someone else if allowed
- tickets (see own tickets)
- a ticket can have workflow attached to it (as set by rhai script)
- on a workflow step needs to be clear for user if they need to do something
- has type, subject, priority, level, comments (user can add comment)
- if new comment message is sent to inbox as well, if new action needed as well
- inbox
- info we can send to the user, user can reply
- has labels
- actions
- if something to do for user, is part of the workflow
- RWDA'S
- overview (in tiles like blogs) of the RWDA's, has tags user can filter
- if user clicks on one then goes to mini site (like ebook), is shown in app
- on RWDA we see nr of RWDA (marketcap, ... and other core financials, ...)
- use can select RWDA, and buy into it, if not enough cash will be asked to put more cash in
- contracts
- as markdown
- user can sign
- see who signed
# user flows
## registration
- login, user choses secret (done by means of the webassembly component)
- verification level, user can do KYC
# rwda
- name
- description
- link to website
- nr of shares
- share value
- vesting period, lockin period
- symbol
# Dynex
- meeting with

View File

@ -0,0 +1,389 @@
use actix_web::{web, HttpResponse, Responder, Result};
use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc};
use serde::{Deserialize, Serialize};
use tera::Tera;
use crate::models::{CalendarEvent, CalendarViewMode};
use crate::utils::RedisCalendarService;
/// Controller for handling calendar-related routes
pub struct CalendarController;
impl CalendarController {
/// Handles the calendar page route
pub async fn calendar(
tmpl: web::Data<Tera>,
query: web::Query<CalendarQuery>,
) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "calendar");
// Parse the view mode from the query parameters
let view_mode = CalendarViewMode::from_str(&query.view.clone().unwrap_or_else(|| "month".to_string()));
ctx.insert("view_mode", &view_mode.to_str());
// Parse the date from the query parameters or use the current date
let date = if let Some(date_str) = &query.date {
match NaiveDate::parse_from_str(date_str, "%Y-%m-%d") {
Ok(naive_date) => Utc.from_utc_date(&naive_date).and_hms_opt(0, 0, 0).unwrap(),
Err(_) => Utc::now(),
}
} else {
Utc::now()
};
ctx.insert("current_date", &date.format("%Y-%m-%d").to_string());
ctx.insert("current_year", &date.year());
ctx.insert("current_month", &date.month());
ctx.insert("current_day", &date.day());
// Get events for the current view
let (start_date, end_date) = match view_mode {
CalendarViewMode::Year => {
let start = Utc.with_ymd_and_hms(date.year(), 1, 1, 0, 0, 0).unwrap();
let end = Utc.with_ymd_and_hms(date.year(), 12, 31, 23, 59, 59).unwrap();
(start, end)
},
CalendarViewMode::Month => {
let start = Utc.with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0).unwrap();
let last_day = Self::last_day_of_month(date.year(), date.month());
let end = Utc.with_ymd_and_hms(date.year(), date.month(), last_day, 23, 59, 59).unwrap();
(start, end)
},
CalendarViewMode::Week => {
// Calculate the start of the week (Sunday)
let weekday = date.weekday().num_days_from_sunday();
let start_date = date.date_naive().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap().pred_opt().unwrap();
let start = Utc.from_utc_date(&start_date).and_hms_opt(0, 0, 0).unwrap();
let end = start + chrono::Duration::days(7);
(start, end)
},
CalendarViewMode::Day => {
let start = Utc.with_ymd_and_hms(date.year(), date.month(), date.day(), 0, 0, 0).unwrap();
let end = Utc.with_ymd_and_hms(date.year(), date.month(), date.day(), 23, 59, 59).unwrap();
(start, end)
},
};
// Get events from Redis
let events = match RedisCalendarService::get_events_in_range(start_date, end_date) {
Ok(events) => events,
Err(e) => {
log::error!("Failed to get events from Redis: {}", e);
vec![]
}
};
ctx.insert("events", &events);
// Generate calendar data based on the view mode
match view_mode {
CalendarViewMode::Year => {
let months = (1..=12).map(|month| {
let month_name = match month {
1 => "January",
2 => "February",
3 => "March",
4 => "April",
5 => "May",
6 => "June",
7 => "July",
8 => "August",
9 => "September",
10 => "October",
11 => "November",
12 => "December",
_ => "",
};
let month_events = events.iter()
.filter(|event| {
event.start_time.month() == month || event.end_time.month() == month
})
.cloned()
.collect::<Vec<_>>();
CalendarMonth {
month,
name: month_name.to_string(),
events: month_events,
}
}).collect::<Vec<_>>();
ctx.insert("months", &months);
},
CalendarViewMode::Month => {
let days_in_month = Self::last_day_of_month(date.year(), date.month());
let first_day = Utc.with_ymd_and_hms(date.year(), date.month(), 1, 0, 0, 0).unwrap();
let first_weekday = first_day.weekday().num_days_from_sunday();
let mut calendar_days = Vec::new();
// Add empty days for the start of the month
for _ in 0..first_weekday {
calendar_days.push(CalendarDay {
day: 0,
events: vec![],
is_current_month: false,
});
}
// Add days for the current month
for day in 1..=days_in_month {
let day_events = events.iter()
.filter(|event| {
let day_start = Utc.with_ymd_and_hms(date.year(), date.month(), day, 0, 0, 0).unwrap();
let day_end = Utc.with_ymd_and_hms(date.year(), date.month(), day, 23, 59, 59).unwrap();
(event.start_time <= day_end && event.end_time >= day_start) ||
(event.all_day && event.start_time.day() <= day && event.end_time.day() >= day)
})
.cloned()
.collect::<Vec<_>>();
calendar_days.push(CalendarDay {
day,
events: day_events,
is_current_month: true,
});
}
// Fill out the rest of the calendar grid (6 rows of 7 days)
let remaining_days = 42 - calendar_days.len();
for day in 1..=remaining_days {
calendar_days.push(CalendarDay {
day: day as u32,
events: vec![],
is_current_month: false,
});
}
ctx.insert("calendar_days", &calendar_days);
ctx.insert("month_name", &Self::month_name(date.month()));
},
CalendarViewMode::Week => {
// Calculate the start of the week (Sunday)
let weekday = date.weekday().num_days_from_sunday();
let week_start = date - chrono::Duration::days(weekday as i64);
let mut week_days = Vec::new();
for i in 0..7 {
let day_date = week_start + chrono::Duration::days(i);
let day_events = events.iter()
.filter(|event| {
let day_start = Utc.with_ymd_and_hms(day_date.year(), day_date.month(), day_date.day(), 0, 0, 0).unwrap();
let day_end = Utc.with_ymd_and_hms(day_date.year(), day_date.month(), day_date.day(), 23, 59, 59).unwrap();
(event.start_time <= day_end && event.end_time >= day_start) ||
(event.all_day && event.start_time.day() <= day_date.day() && event.end_time.day() >= day_date.day())
})
.cloned()
.collect::<Vec<_>>();
week_days.push(CalendarDay {
day: day_date.day(),
events: day_events,
is_current_month: day_date.month() == date.month(),
});
}
ctx.insert("week_days", &week_days);
},
CalendarViewMode::Day => {
log::info!("Day view selected");
ctx.insert("day_name", &Self::day_name(date.weekday().num_days_from_sunday()));
// Add debug info
log::info!("Events count: {}", events.len());
log::info!("Current date: {}", date.format("%Y-%m-%d"));
log::info!("Day name: {}", Self::day_name(date.weekday().num_days_from_sunday()));
},
}
let rendered = tmpl.render("calendar/index.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
}
/// Handles the new event page route
pub async fn new_event(tmpl: web::Data<Tera>) -> Result<impl Responder> {
let mut ctx = tera::Context::new();
ctx.insert("active_page", "calendar");
let rendered = tmpl.render("calendar/new_event.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::Ok().content_type("text/html").body(rendered))
}
/// Handles the create event route
pub async fn create_event(
form: web::Form<EventForm>,
tmpl: web::Data<Tera>,
) -> Result<impl Responder> {
// Parse the start and end times
let start_time = match DateTime::parse_from_rfc3339(&form.start_time) {
Ok(dt) => dt.with_timezone(&Utc),
Err(e) => {
log::error!("Failed to parse start time: {}", e);
return Ok(HttpResponse::BadRequest().body("Invalid start time"));
}
};
let end_time = match DateTime::parse_from_rfc3339(&form.end_time) {
Ok(dt) => dt.with_timezone(&Utc),
Err(e) => {
log::error!("Failed to parse end time: {}", e);
return Ok(HttpResponse::BadRequest().body("Invalid end time"));
}
};
// Create the event
let event = CalendarEvent::new(
form.title.clone(),
form.description.clone(),
start_time,
end_time,
Some(form.color.clone()),
form.all_day,
None, // User ID would come from session in a real app
);
// Save the event to Redis
match RedisCalendarService::save_event(&event) {
Ok(_) => {
// Redirect to the calendar page
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/calendar"))
.finish())
},
Err(e) => {
log::error!("Failed to save event to Redis: {}", e);
// Show an error message
let mut ctx = tera::Context::new();
ctx.insert("active_page", "calendar");
ctx.insert("error", "Failed to save event");
let rendered = tmpl.render("calendar/new_event.html", &ctx)
.map_err(|e| {
eprintln!("Template rendering error: {}", e);
actix_web::error::ErrorInternalServerError("Template rendering error")
})?;
Ok(HttpResponse::InternalServerError().content_type("text/html").body(rendered))
}
}
}
/// Handles the delete event route
pub async fn delete_event(
path: web::Path<String>,
) -> Result<impl Responder> {
let id = path.into_inner();
// Delete the event from Redis
match RedisCalendarService::delete_event(&id) {
Ok(_) => {
// Redirect to the calendar page
Ok(HttpResponse::SeeOther()
.append_header(("Location", "/calendar"))
.finish())
},
Err(e) => {
log::error!("Failed to delete event from Redis: {}", e);
Ok(HttpResponse::InternalServerError().body("Failed to delete event"))
}
}
}
/// Returns the last day of the month
fn last_day_of_month(year: i32, month: u32) -> u32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
4 | 6 | 9 | 11 => 30,
2 => {
if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 {
29
} else {
28
}
},
_ => 30, // Default to 30 days
}
}
/// Returns the name of the month
fn month_name(month: u32) -> &'static str {
match month {
1 => "January",
2 => "February",
3 => "March",
4 => "April",
5 => "May",
6 => "June",
7 => "July",
8 => "August",
9 => "September",
10 => "October",
11 => "November",
12 => "December",
_ => "",
}
}
/// Returns the name of the day
fn day_name(day: u32) -> &'static str {
match day {
0 => "Sunday",
1 => "Monday",
2 => "Tuesday",
3 => "Wednesday",
4 => "Thursday",
5 => "Friday",
6 => "Saturday",
_ => "",
}
}
}
/// Query parameters for the calendar page
#[derive(Debug, Deserialize)]
pub struct CalendarQuery {
pub view: Option<String>,
pub date: Option<String>,
}
/// Form data for creating an event
#[derive(Debug, Deserialize)]
pub struct EventForm {
pub title: String,
pub description: String,
pub start_time: String,
pub end_time: String,
pub color: String,
pub all_day: bool,
}
/// Represents a day in the calendar
#[derive(Debug, Serialize)]
struct CalendarDay {
day: u32,
events: Vec<CalendarEvent>,
is_current_month: bool,
}
/// Represents a month in the calendar
#[derive(Debug, Serialize)]
struct CalendarMonth {
month: u32,
name: String,
events: Vec<CalendarEvent>,
}

View File

@ -2,8 +2,10 @@
pub mod home; pub mod home;
pub mod auth; pub mod auth;
pub mod ticket; pub mod ticket;
pub mod calendar;
// Re-export controllers for easier imports // Re-export controllers for easier imports
pub use home::HomeController; pub use home::HomeController;
pub use auth::AuthController; pub use auth::AuthController;
pub use ticket::TicketController; pub use ticket::TicketController;
pub use calendar::CalendarController;

View File

@ -13,6 +13,7 @@ mod utils;
// Import middleware components // Import middleware components
use app_middleware::{RequestTimer, SecurityHeaders}; use app_middleware::{RequestTimer, SecurityHeaders};
use utils::redis_service;
// Initialize lazy_static for in-memory storage // Initialize lazy_static for in-memory storage
#[macro_use] #[macro_use]
@ -28,6 +29,15 @@ async fn main() -> io::Result<()> {
let config = config::get_config(); let config = config::get_config();
let bind_address = format!("{}:{}", config.server.host, config.server.port); let bind_address = format!("{}:{}", config.server.host, config.server.port);
// Initialize Redis client
let redis_url = std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string());
if let Err(e) = redis_service::init_redis_client(&redis_url) {
log::error!("Failed to initialize Redis client: {}", e);
log::warn!("Calendar functionality will not work properly without Redis");
} else {
log::info!("Redis client initialized successfully");
}
log::info!("Starting server at http://{}", bind_address); log::info!("Starting server at http://{}", bind_address);
// Create and configure the HTTP server // Create and configure the HTTP server

View File

@ -0,0 +1,94 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// Represents a calendar event
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalendarEvent {
/// Unique identifier for the event
pub id: String,
/// Title of the event
pub title: String,
/// Description of the event
pub description: String,
/// Start time of the event
pub start_time: DateTime<Utc>,
/// End time of the event
pub end_time: DateTime<Utc>,
/// Color of the event (hex code)
pub color: String,
/// Whether the event is an all-day event
pub all_day: bool,
/// User ID of the event creator
pub user_id: Option<String>,
}
impl CalendarEvent {
/// Creates a new calendar event
pub fn new(
title: String,
description: String,
start_time: DateTime<Utc>,
end_time: DateTime<Utc>,
color: Option<String>,
all_day: bool,
user_id: Option<String>,
) -> Self {
Self {
id: Uuid::new_v4().to_string(),
title,
description,
start_time,
end_time,
color: color.unwrap_or_else(|| "#4285F4".to_string()), // Google Calendar blue
all_day,
user_id,
}
}
/// Converts the event to a JSON string
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
/// Creates an event from a JSON string
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
}
/// Represents a view mode for the calendar
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CalendarViewMode {
/// Year view
Year,
/// Month view
Month,
/// Week view
Week,
/// Day view
Day,
}
impl CalendarViewMode {
/// Converts a string to a view mode
pub fn from_str(s: &str) -> Self {
match s.to_lowercase().as_str() {
"year" => Self::Year,
"month" => Self::Month,
"week" => Self::Week,
"day" => Self::Day,
_ => Self::Month, // Default to month view
}
}
/// Converts the view mode to a string
pub fn to_str(&self) -> &'static str {
match self {
Self::Year => "year",
Self::Month => "month",
Self::Week => "week",
Self::Day => "day",
}
}
}

View File

@ -1,7 +1,9 @@
// Export models // Export models
pub mod user; pub mod user;
pub mod ticket; pub mod ticket;
pub mod calendar;
// Re-export models for easier imports // Re-export models for easier imports
pub use user::User; pub use user::User;
pub use ticket::{Ticket, TicketComment, TicketStatus, TicketPriority, TicketFilter}; pub use ticket::{Ticket, TicketComment, TicketStatus, TicketPriority, TicketFilter};
pub use calendar::{CalendarEvent, CalendarViewMode};

View File

@ -4,6 +4,7 @@ use actix_web::cookie::Key;
use crate::controllers::home::HomeController; use crate::controllers::home::HomeController;
use crate::controllers::auth::AuthController; use crate::controllers::auth::AuthController;
use crate::controllers::ticket::TicketController; use crate::controllers::ticket::TicketController;
use crate::controllers::calendar::CalendarController;
/// Configures all application routes /// Configures all application routes
pub fn configure_routes(cfg: &mut web::ServiceConfig) { pub fn configure_routes(cfg: &mut web::ServiceConfig) {
@ -42,5 +43,11 @@ pub fn configure_routes(cfg: &mut web::ServiceConfig) {
.route("/tickets/{id}", web::get().to(TicketController::show_ticket)) .route("/tickets/{id}", web::get().to(TicketController::show_ticket))
.route("/tickets/{id}/comment", web::post().to(TicketController::add_comment)) .route("/tickets/{id}/comment", web::post().to(TicketController::add_comment))
.route("/tickets/{id}/status/{status}", web::get().to(TicketController::update_status)) .route("/tickets/{id}/status/{status}", web::get().to(TicketController::update_status))
// Calendar routes
.route("/calendar", web::get().to(CalendarController::calendar))
.route("/calendar/new", web::get().to(CalendarController::new_event))
.route("/calendar/new", web::post().to(CalendarController::create_event))
.route("/calendar/{id}/delete", web::get().to(CalendarController::delete_event))
); );
} }

View File

@ -1,6 +1,12 @@
use chrono::{DateTime, TimeZone, Utc}; use chrono::{DateTime, TimeZone, Utc};
use tera::{self, Function, Result, Value}; use tera::{self, Function, Result, Value};
// Export modules
pub mod redis_service;
// Re-export for easier imports
pub use redis_service::RedisCalendarService;
/// Registers custom Tera functions /// Registers custom Tera functions
pub fn register_tera_functions(tera: &mut tera::Tera) { pub fn register_tera_functions(tera: &mut tera::Tera) {
tera.register_function("now", NowFunction); tera.register_function("now", NowFunction);

View File

@ -0,0 +1,144 @@
use redis::{Client, Commands, Connection, RedisError};
use std::sync::{Arc, Mutex};
use lazy_static::lazy_static;
use crate::models::CalendarEvent;
// Create a lazy static Redis client that can be used throughout the application
lazy_static! {
static ref REDIS_CLIENT: Arc<Mutex<Option<Client>>> = Arc::new(Mutex::new(None));
}
/// Initialize the Redis client
pub fn init_redis_client(redis_url: &str) -> Result<(), RedisError> {
let client = redis::Client::open(redis_url)?;
// Test the connection
let _: Connection = client.get_connection()?;
// Store the client in the lazy static
let mut client_guard = REDIS_CLIENT.lock().unwrap();
*client_guard = Some(client);
Ok(())
}
/// Get a Redis connection
pub fn get_connection() -> Result<Connection, RedisError> {
let client_guard = REDIS_CLIENT.lock().unwrap();
if let Some(client) = &*client_guard {
client.get_connection()
} else {
Err(RedisError::from(std::io::Error::new(
std::io::ErrorKind::NotConnected,
"Redis client not initialized",
)))
}
}
/// Redis service for calendar events
pub struct RedisCalendarService;
impl RedisCalendarService {
/// Key prefix for calendar events
const EVENT_KEY_PREFIX: &'static str = "calendar:event:";
/// Key for the set of all event IDs
const ALL_EVENTS_KEY: &'static str = "calendar:all_events";
/// Save a calendar event to Redis
pub fn save_event(event: &CalendarEvent) -> Result<(), RedisError> {
let mut conn = get_connection()?;
// Convert the event to JSON
let json = event.to_json().map_err(|e| {
RedisError::from(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Failed to serialize event: {}", e),
))
})?;
// Save the event
let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, event.id);
let _: () = conn.set(event_key, json)?;
// Add the event ID to the set of all events
let _: () = conn.sadd(Self::ALL_EVENTS_KEY, &event.id)?;
Ok(())
}
/// Get a calendar event from Redis by ID
pub fn get_event(id: &str) -> Result<Option<CalendarEvent>, RedisError> {
let mut conn = get_connection()?;
// Get the event JSON
let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, id);
let json: Option<String> = conn.get(event_key)?;
// Parse the JSON
if let Some(json) = json {
let event = CalendarEvent::from_json(&json).map_err(|e| {
RedisError::from(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Failed to deserialize event: {}", e),
))
})?;
Ok(Some(event))
} else {
Ok(None)
}
}
/// Delete a calendar event from Redis
pub fn delete_event(id: &str) -> Result<bool, RedisError> {
let mut conn = get_connection()?;
// Delete the event
let event_key = format!("{}{}", Self::EVENT_KEY_PREFIX, id);
let deleted: i32 = conn.del(event_key)?;
// Remove the event ID from the set of all events
let _: () = conn.srem(Self::ALL_EVENTS_KEY, id)?;
Ok(deleted > 0)
}
/// Get all calendar events from Redis
pub fn get_all_events() -> Result<Vec<CalendarEvent>, RedisError> {
let mut conn = get_connection()?;
// Get all event IDs
let event_ids: Vec<String> = conn.smembers(Self::ALL_EVENTS_KEY)?;
// Get all events
let mut events = Vec::new();
for id in event_ids {
if let Some(event) = Self::get_event(&id)? {
events.push(event);
}
}
Ok(events)
}
/// Get events for a specific date range
pub fn get_events_in_range(
start: chrono::DateTime<chrono::Utc>,
end: chrono::DateTime<chrono::Utc>,
) -> Result<Vec<CalendarEvent>, RedisError> {
let all_events = Self::get_all_events()?;
// Filter events that fall within the date range
let filtered_events = all_events
.into_iter()
.filter(|event| {
// Check if the event overlaps with the date range
(event.start_time <= end && event.end_time >= start)
})
.collect();
Ok(filtered_events)
}
}

View File

@ -32,6 +32,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link {% if active_page == 'editor' %}active{% endif %}" href="/editor">Markdown Editor</a> <a class="nav-link {% if active_page == 'editor' %}active{% endif %}" href="/editor">Markdown Editor</a>
</li> </li>
<li class="nav-item">
<a class="nav-link {% if active_page == 'calendar' %}active{% endif %}" href="/calendar">Calendar</a>
</li>
</ul> </ul>
<ul class="navbar-nav ms-auto"> <ul class="navbar-nav ms-auto">
{% if user and user.id %} {% if user and user.id %}

View File

@ -0,0 +1,132 @@
{% extends "base.html" %}
{% block title %}Calendar{% endblock %}
{% block content %}
<div class="container">
<h1>Calendar</h1>
<p>View Mode: {{ view_mode }}</p>
<p>Current Date: {{ current_date }}</p>
<div class="d-flex justify-content-between align-items-center mb-3">
<div class="btn-group">
<a href="/calendar?view=day" class="btn btn-outline-primary">Day</a>
<a href="/calendar?view=month" class="btn btn-outline-primary">Month</a>
<a href="/calendar?view=year" class="btn btn-outline-primary">Year</a>
</div>
<a href="/calendar/new" class="btn btn-success">
<i class="bi bi-plus-circle"></i> Create New Event
</a>
</div>
{% if view_mode == "month" %}
<h2>Month View: {{ month_name }} {{ current_year }}</h2>
<table class="table table-bordered">
<thead>
<tr>
<th>Sun</th>
<th>Mon</th>
<th>Tue</th>
<th>Wed</th>
<th>Thu</th>
<th>Fri</th>
<th>Sat</th>
</tr>
</thead>
<tbody>
{% for week in range(start=0, end=6) %}
<tr>
{% for day_idx in range(start=0, end=7) %}
<td>
{% set idx = week * 7 + day_idx %}
{% if idx < calendar_days|length %}
{% set day = calendar_days[idx] %}
{% if day.day > 0 %}
{{ day.day }}
{% endif %}
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
{% elif view_mode == "year" %}
<h2>Year View: {{ current_year }}</h2>
<div class="row">
{% for month in months %}
<div class="col-md-4 mb-3">
<div class="card">
<div class="card-header">{{ month.name }}</div>
<div class="card-body">
<p>Events: {{ month.events|length }}</p>
</div>
</div>
</div>
{% endfor %}
</div>
{% elif view_mode == "day" %}
<h2>Day View: {{ current_date }}</h2>
<div class="card mb-4">
<div class="card-header bg-primary text-white">
All Day Events
</div>
<div class="card-body">
{% if events is defined and events|length > 0 %}
{% for event in events %}
{% if event.all_day %}
<div class="alert" style="background-color: {{ event.color }}; color: white;">
<h5>{{ event.title }}</h5>
<p>{{ event.description }}</p>
</div>
{% endif %}
{% endfor %}
{% else %}
<p class="text-muted">No all-day events</p>
{% endif %}
</div>
</div>
<div class="list-group">
{% for hour in range(start=0, end=24) %}
<div class="list-group-item">
<div class="d-flex">
<div class="pe-3" style="width: 60px; text-align: right;">
<strong>{{ "%02d"|format(value=hour) }}:00</strong>
</div>
<div class="flex-grow-1">
{% if events is defined and events|length > 0 %}
{% for event in events %}
{% if not event.all_day %}
{% set start_hour = event.start_time|date(format="%H") %}
{% if start_hour == hour|string %}
<div class="alert mb-2" style="background-color: {{ event.color }}; color: white;">
<h5>{{ event.title }}</h5>
<p>{{ event.start_time|date(format="%H:%M") }} - {{ event.end_time|date(format="%H:%M") }}</p>
<p>{{ event.description }}</p>
</div>
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Floating Action Button (FAB) for creating new events -->
<a href="/calendar/new" class="position-fixed bottom-0 end-0 m-4 btn btn-primary rounded-circle shadow" style="width: 60px; height: 60px; display: flex; align-items: center; justify-content: center; font-size: 24px;">
<i class="bi bi-plus"></i>
</a>
</div>
{% block extra_css %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
{% endblock %}
{% endblock %}

View File

@ -0,0 +1,98 @@
{% extends "base.html" %}
{% block title %}New Calendar Event{% endblock %}
{% block content %}
<div class="container">
<h1>Create New Event</h1>
{% if error %}
<div class="alert alert-danger" role="alert">
{{ error }}
</div>
{% endif %}
<form action="/calendar/new" method="post">
<div class="mb-3">
<label for="title" class="form-label">Event Title</label>
<input type="text" class="form-control" id="title" name="title" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description" rows="3"></textarea>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="all_day" name="all_day">
<label class="form-check-label" for="all_day">All Day Event</label>
</div>
<div class="row mb-3">
<div class="col">
<label for="start_time" class="form-label">Start Time</label>
<input type="datetime-local" class="form-control" id="start_time" name="start_time" required>
</div>
<div class="col">
<label for="end_time" class="form-label">End Time</label>
<input type="datetime-local" class="form-control" id="end_time" name="end_time" required>
</div>
</div>
<div class="mb-3">
<label for="color" class="form-label">Event Color</label>
<select class="form-control" id="color" name="color">
<option value="#4285F4">Blue</option>
<option value="#EA4335">Red</option>
<option value="#34A853">Green</option>
<option value="#FBBC05">Yellow</option>
<option value="#A142F4">Purple</option>
<option value="#24C1E0">Cyan</option>
</select>
</div>
<div class="mb-3">
<button type="submit" class="btn btn-primary">Create Event</button>
<a href="/calendar" class="btn btn-secondary">Cancel</a>
</div>
</form>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Convert datetime-local inputs to RFC3339 format on form submission
document.querySelector('form').addEventListener('submit', function(e) {
e.preventDefault();
const startTime = document.getElementById('start_time').value;
const endTime = document.getElementById('end_time').value;
// Convert to RFC3339 format
const startRFC = new Date(startTime).toISOString();
const endRFC = new Date(endTime).toISOString();
// Create hidden inputs for the RFC3339 values
const startInput = document.createElement('input');
startInput.type = 'hidden';
startInput.name = 'start_time';
startInput.value = startRFC;
const endInput = document.createElement('input');
endInput.type = 'hidden';
endInput.name = 'end_time';
endInput.value = endRFC;
// Remove the original inputs
document.getElementById('start_time').removeAttribute('name');
document.getElementById('end_time').removeAttribute('name');
// Add the hidden inputs to the form
this.appendChild(startInput);
this.appendChild(endInput);
// Submit the form
this.submit();
});
});
</script>
{% endblock %}