···55timeline-view = Timeline
66calendar-view = Calendar
77filter-by-tags = Filter by Tags
88-calendar-navigation = Calendar
98remove-bookmark = Remove Bookmark
109confirm-remove-bookmark = Are you sure you want to remove this bookmark?
1110add-to-calendar = Add to Calendar
···1514bookmark-tags = Tags (comma-separated)
1615bookmark-calendar-name = Calendar Name
1716bookmark-calendar-description = Description (optional)
1818-make-calendar-public = Make this calendar public
1917bookmarked-on = Bookmarked on {$date}
2020-ends-at = Ends at {$time}
21182219# Enhanced calendar management
2320bookmark-calendars = Custom Calendars
2421create-calendar = Create Calendar
2522create-bookmark-calendar = Create Custom Calendar
2626-create-new-calendar = Create New Calendar
2723calendar-name = Calendar Name
2824calendar-name-placeholder = e.g. Summer Festivals, Work Events
2925calendar-name-help = Choose a descriptive name for your calendar
3026calendar-tags = Tags
3127add-tag-placeholder = Add a tag and press Enter
3232-calendar-tags-help = Add tags to organize your calendar
3328calendar-description-placeholder = What types of events will you collect?
3429make-calendar-public = Make this calendar public
3530public-calendar-help = Public calendars appear on your profile and can be discovered by others
···4035no-public-calendars = No public calendars yet
4136export-calendar = Export Calendar
4237share-calendar = Share Calendar
4343-events = events
4438created = Created on
4539calendar-updated = Calendar updated successfully
4640calendar-created = Calendar created successfully
4741event-added-to-calendar = Event added to calendar
48424343+# Timeline view
4444+back-to-calendars = Back to Calendars
4545+calendar-navigation = Calendar
4646+start-bookmarking-events = Start bookmarking events to see them here
4747+4948# Tag management
5049calendar-tags-help = Add multiple tags to organize this calendar (press Enter or comma to add)
5150tag-suggestions = Suggested tags
···6160error-max-tags = Maximum 10 tags allowed per calendar
6261error-empty-tag = Empty tags are not allowed
6362error-tag-too-long = Tag too long (maximum 50 characters)
6363+6464+# Calendar management
6565+manage-calendars = Manage Calendars
6666+view-calendar = View Calendar
6767+delete-calendar = Delete Calendar
6868+confirm-delete-calendar = Are you sure you want to delete this calendar?
6969+no-calendars-yet = No calendars yet
7070+create-first-calendar-help = Create your first calendar to organize bookmarked events by tags
+16
i18n/en-us/common.ftl
···311311# Location labels
312312location = Location
313313314314+# Navigation for bookmark integration
315315+nav-events-calendars = Events & Calendars
316316+nav-browse-events = Browse Events
317317+nav-my-calendars = My Calendars
318318+nav-all-bookmarks = All Bookmarks
319319+320320+# Calendar days and months
321321+mon = Mon
322322+tue = Tue
323323+wed = Wed
324324+thu = Thu
325325+fri = Fri
326326+sat = Sat
327327+sun = Sun
328328+june = June
329329+314330# iCal export translations
315331ical-untitled-event = Untitled Event
316332ical-event-description-fallback = Event organized by {$organizer} via smokesignal.events
+13-6
i18n/fr-ca/bookmarks.ftl
···55timeline-view = Vue chronologique
66calendar-view = Vue calendrier
77filter-by-tags = Filtrer par étiquettes
88-calendar-navigation = Navigation
98remove-bookmark = Retirer le marque-page
109confirm-remove-bookmark = Êtes-vous sûr·e de vouloir retirer ce marque-page?
1110add-to-calendar = Ajouter au calendrier
1212-create-new-calendar = Créer un nouveau calendrier
1311bookmark-success = Événement marqué avec succès!
1412no-bookmarked-events = Aucun événement marqué trouvé
1513bookmark-tags = Étiquettes (séparées par des virgules)
1614bookmark-calendar-name = Nom du calendrier
1715bookmark-calendar-description = Description (optionnelle)
1818-make-calendar-public = Rendre ce calendrier public
1916bookmarked-on = Marqué le {$date}
2020-ends-at = Se termine à {$time}
21172218# Gestion avancée des calendriers
2319bookmark-calendars = Calendriers personnalisés
···2925calendar-name-help = Choisissez un nom descriptif pour votre calendrier
3026calendar-tags = Étiquettes
3127add-tag-placeholder = Ajouter une étiquette et appuyer sur Entrée
3232-calendar-tags-help = Ajoutez des étiquettes pour organiser votre calendrier
3328calendar-description-placeholder = Quels types d'événements allez-vous collecter ?
3429make-calendar-public = Rendre ce calendrier public
3530public-calendar-help = Les calendriers publics apparaissent sur votre profil et peuvent être découverts par d'autres
···4035no-public-calendars = Pas encore de calendriers publics
4136export-calendar = Exporter le calendrier
4237share-calendar = Partager le calendrier
4343-events = événements
4438created = Créé le
4539calendar-updated = Calendrier mis à jour avec succès
4640calendar-created = Calendrier créé avec succès
4741event-added-to-calendar = Événement ajouté au calendrier
48424343+# Vue chronologique
4444+back-to-calendars = Retour aux calendriers
4545+calendar-navigation = Navigation
4646+start-bookmarking-events = Commencez à marquer des événements pour les voir ici
4747+4948# Gestion des étiquettes
5049calendar-tags-help = Ajoutez plusieurs étiquettes pour organiser ce calendrier (appuyez sur Entrée ou virgule pour ajouter)
5150tag-suggestions = Étiquettes suggérées
···6160error-max-tags = Maximum 10 étiquettes autorisées par calendrier
6261error-empty-tag = Les étiquettes vides ne sont pas autorisées
6362error-tag-too-long = Étiquette trop longue (maximum 50 caractères)
6363+6464+# Gestion des calendriers
6565+manage-calendars = Gérer les calendriers
6666+view-calendar = Voir le calendrier
6767+delete-calendar = Supprimer le calendrier
6868+confirm-delete-calendar = Êtes-vous sûr·e de vouloir supprimer ce calendrier ?
6969+no-calendars-yet = Aucun calendrier pour l'instant
7070+create-first-calendar-help = Créez votre premier calendrier pour organiser les événements marqués par étiquettes
+16
i18n/fr-ca/common.ftl
···330330# Étiquettes de lieu
331331location = Lieu
332332333333+# Navigation pour l'intégration des signets
334334+nav-events-calendars = Événements et calendriers
335335+nav-browse-events = Parcourir les événements
336336+nav-my-calendars = Mes calendriers
337337+nav-all-bookmarks = Tous les signets
338338+339339+# Jours et mois du calendrier
340340+mon = Lun
341341+tue = Mar
342342+wed = Mer
343343+thu = Jeu
344344+fri = Ven
345345+sat = Sam
346346+sun = Dim
347347+june = Juin
348348+333349# Traductions d'exportation iCal
334350ical-untitled-event = Événement sans titre
335351ical-event-description-fallback = Événement organisé par {$organizer} via smokesignal.events
···11pub mod com_atproto_repo;
22-mod community_lexicon_bookmarks;
22+pub mod community_lexicon_bookmarks;
33mod community_lexicon_calendar_event;
44mod community_lexicon_calendar_rsvp;
55pub mod community_lexicon_location;
+20-23
src/http/handle_bookmark_calendars.rs
···11use axum::{
22 extract::{Path},
33- response::{Html, IntoResponse},
33+ response::{Html, IntoResponse, Redirect},
44 http::StatusCode,
55 Json,
66};
···4343 let language_clone = ctx.language.clone();
4444 let renderer = create_renderer!(ctx.web_context.clone(), language_clone, hx_boosted, false);
45454646- // For now, return empty calendars list
4646+ // Fetch user's calendars from the database
4747+ let calendars = match crate::storage::bookmark_calendars::get_by_user(&ctx.web_context.pool, ¤t_handle.did, true).await {
4848+ Ok(calendars) => calendars,
4949+ Err(e) => {
5050+ error!("Failed to fetch calendars for user {}: {}", current_handle.did, e);
5151+ Vec::new()
5252+ }
5353+ };
5454+4755 let template_context = template_context! {
4848- calendars => Vec::<BookmarkCalendar>::new(),
5656+ calendars => calendars,
4957 user_did => current_handle.did,
5058 };
5159···105113 Ok(created_calendar) => {
106114 info!("Successfully created calendar {} for user {}", created_calendar.calendar_id, current_handle.did);
107115108108- let html = format!(
109109- r#"<div class="notification is-success">
110110- <p>Calendar "{}" created successfully!</p>
111111- </div>"#,
112112- created_calendar.name
113113- );
114114-115115- Ok(Html(html).into_response())
116116+ // Return a simple success response - JavaScript will handle refreshing the page
117117+ Ok(Html("Calendar created successfully".to_string()).into_response())
116118 }
117119 Err(e) => {
118120 error!("Failed to create calendar: {}", e);
···140142 return Ok((StatusCode::FORBIDDEN, "Access denied".to_string()).into_response());
141143 }
142144143143- let template_context = template_context! {
144144- calendar => calendar,
145145- is_owner => calendar.did == current_handle.did,
146146- events => Vec::<String>::new(), // TODO: Fetch events for this calendar
147147- };
148148-149149- let html = renderer.render_template(
150150- "bookmark_calendar_view",
151151- template_context,
152152- ctx.current_handle.as_ref(),
153153- &format!("/bookmark-calendars/{}", calendar_id)
145145+ // Redirect to the bookmark timeline with the calendar's tags as filters
146146+ let tags_param = calendar.tags.join(",");
147147+ let redirect_url = format!("/bookmarks?tags={}&calendar_name={}",
148148+ urlencoding::encode(&tags_param),
149149+ urlencoding::encode(&calendar.name)
154150 );
155155- Ok(Html(html).into_response())
151151+152152+ Ok(axum::response::Redirect::to(&redirect_url).into_response())
156153 }
157154 Ok(None) => {
158155 Ok((StatusCode::NOT_FOUND, "Calendar not found".to_string()).into_response())
+248-33
src/http/handle_bookmark_events.rs
···44 http::StatusCode,
55};
66use axum_htmx::HxBoosted;
77-use chrono::Datelike;
77+use chrono::{Datelike, DateTime, TimeZone, Timelike};
88+use chrono_tz::Tz;
89use minijinja::context as template_context;
910use serde::{Deserialize, Serialize};
1011use std::sync::Arc;
···1314use crate::create_renderer;
1415use crate::http::context::UserRequestContext;
1516use crate::http::errors::WebError;
1717+use crate::i18n::fluent_loader::LOCALES;
1618use crate::services::event_bookmarks::EventBookmarkService;
1919+use crate::storage::event_bookmarks::BookmarkedEvent;
2020+use fluent_templates::Loader;
2121+2222+// Date formatting function with French locale support
2323+fn format_datetime_for_locale(dt: &DateTime<Tz>, locale: Option<&fluent_templates::LanguageIdentifier>) -> String {
2424+ // Check if we're in French locale
2525+ let is_french = locale
2626+ .map(|l| l.language.as_str() == "fr")
2727+ .unwrap_or(false);
2828+2929+ if is_french {
3030+ // French format
3131+ let months_fr = [
3232+ "janvier", "février", "mars", "avril", "mai", "juin",
3333+ "juillet", "août", "septembre", "octobre", "novembre", "décembre"
3434+ ];
3535+ let month_name = months_fr.get((dt.month() - 1) as usize).unwrap_or(&"");
3636+ format!("{} {} {} à {}:{:02}",
3737+ dt.day(),
3838+ month_name,
3939+ dt.year(),
4040+ dt.hour(),
4141+ dt.minute()
4242+ )
4343+ } else {
4444+ // English format
4545+ dt.format("%B %-d, %Y at %-I:%M %p").to_string()
4646+ }
4747+}
4848+4949+// Enhanced BookmarkedEvent with timezone-aware formatting
5050+#[derive(Serialize)]
5151+struct BookmarkedEventWithTz {
5252+ #[serde(flatten)]
5353+ event: BookmarkedEvent,
5454+ starts_at_human: Option<String>,
5555+ ends_at_human: Option<String>,
5656+}
5757+5858+fn process_bookmarked_events_with_timezone(events: Vec<BookmarkedEvent>, tz: &Tz, locale: Option<&fluent_templates::LanguageIdentifier>) -> Vec<BookmarkedEventWithTz> {
5959+ events.into_iter().map(|event| {
6060+ let starts_at_human = event.event_details.starts_at.as_ref().map(|dt| {
6161+ let dt_with_tz = dt.with_timezone(tz);
6262+ format_datetime_for_locale(&dt_with_tz, locale)
6363+ });
6464+6565+ let ends_at_human = event.event_details.ends_at.as_ref().map(|dt| {
6666+ let dt_with_tz = dt.with_timezone(tz);
6767+ format_datetime_for_locale(&dt_with_tz, locale)
6868+ });
6969+7070+ BookmarkedEventWithTz {
7171+ event,
7272+ starts_at_human,
7373+ ends_at_human,
7474+ }
7575+ }).collect()
7676+}
17771878#[derive(Deserialize)]
1979pub struct BookmarkEventParams {
···47107) -> Result<impl IntoResponse, WebError> {
48108 let current_handle = ctx.auth.require(&ctx.web_context.config.destination_key, "/bookmarks")?;
49109110110+ // Debug logging to track tag parsing
111111+ info!("Raw tags parameter: {:?}", params.tags);
112112+50113 let tags: Vec<String> = params
51114 .tags
52115 .unwrap_or_default()
···54117 .map(|s| s.trim().to_string())
55118 .filter(|s| !s.is_empty())
56119 .collect();
120120+121121+ info!("Parsed tags: {:?}", tags);
5712258123 // Validate tags
59124 if tags.len() > 10 {
6060- return Ok((StatusCode::BAD_REQUEST, "Maximum 10 tags allowed".to_string()).into_response());
125125+ let error_message = LOCALES.lookup(&ctx.language.0, "error-max-tags");
126126+ return Ok((StatusCode::BAD_REQUEST, error_message).into_response());
61127 }
6212863129 for tag in &tags {
64130 if tag.len() > 50 {
6565- return Ok((StatusCode::BAD_REQUEST, "Tag too long (maximum 50 characters)".to_string()).into_response());
131131+ let error_message = LOCALES.lookup(&ctx.language.0, "error-tag-too-long");
132132+ return Ok((StatusCode::BAD_REQUEST, error_message).into_response());
66133 }
67134 }
68135···71138 Arc::new(ctx.web_context.atrium_oauth_manager.clone()),
72139 );
73140141141+ // First check if the event is already bookmarked
74142 match bookmark_service
7575- .bookmark_event(¤t_handle.did, ¶ms.event_aturi, tags)
143143+ .get_bookmark(¤t_handle.did, ¶ms.event_aturi)
76144 .await
77145 {
7878- Ok(_bookmark) => {
7979- info!("Successfully bookmarked event {} for user {}", params.event_aturi, current_handle.did);
8080-8181- let html = r#"<div class="notification is-success">
8282- <p>Event bookmarked successfully!</p>
8383- </div>"#;
8484-8585- Ok(Html(html.to_string()).into_response())
146146+ Ok(Some(_existing_bookmark)) => {
147147+ // Event is already bookmarked, update the tags
148148+ // Get session group from the Auth context
149149+ let session_group = ctx.auth.1.as_ref()
150150+ .map(|oauth_session| oauth_session.session_group.as_str())
151151+ .unwrap_or("unknown"); // Fallback if no session
152152+153153+ match bookmark_service
154154+ .update_bookmark_tags(¤t_handle.did, ¶ms.event_aturi, tags, session_group)
155155+ .await
156156+ {
157157+ Ok(_) => {
158158+ info!("Successfully updated bookmark tags for event {} for user {}", params.event_aturi, current_handle.did);
159159+160160+ let success_message = LOCALES.lookup(&ctx.language.0, "calendar-updated");
161161+ let html = format!(r#"<div class="notification is-success">
162162+ <p>{}</p>
163163+ </div>"#, success_message);
164164+165165+ Ok(Html(html).into_response())
166166+ }
167167+ Err(e) => {
168168+ error!("Failed to update bookmark tags for event {}: {}", params.event_aturi, e);
169169+ let error_message = LOCALES.lookup(&ctx.language.0, "error-occurred");
170170+ Ok((StatusCode::INTERNAL_SERVER_ERROR, error_message).into_response())
171171+ }
172172+ }
173173+ }
174174+ Ok(None) => {
175175+ // Event is not bookmarked, create new bookmark
176176+ // Get session group from the Auth context
177177+ let session_group = ctx.auth.1.as_ref()
178178+ .map(|oauth_session| oauth_session.session_group.as_str())
179179+ .unwrap_or("unknown"); // Fallback if no session
180180+181181+ match bookmark_service
182182+ .bookmark_event(¤t_handle.did, ¶ms.event_aturi, tags, session_group)
183183+ .await
184184+ {
185185+ Ok(_bookmark) => {
186186+ info!("Successfully bookmarked event {} for user {}", params.event_aturi, current_handle.did);
187187+188188+ let success_message = LOCALES.lookup(&ctx.language.0, "bookmark-success");
189189+ let html = format!(r#"<div class="notification is-success">
190190+ <p>{}</p>
191191+ </div>"#, success_message);
192192+193193+ Ok(Html(html).into_response())
194194+ }
195195+ Err(e) => {
196196+ error!("Failed to bookmark event {}: {}", params.event_aturi, e);
197197+ let error_message = LOCALES.lookup(&ctx.language.0, "error-occurred");
198198+ Ok((StatusCode::INTERNAL_SERVER_ERROR, error_message).into_response())
199199+ }
200200+ }
86201 }
87202 Err(e) => {
8888- error!("Failed to bookmark event {}: {}", params.event_aturi, e);
8989- Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to bookmark event".to_string()).into_response())
203203+ error!("Failed to check existing bookmark for event {}: {}", params.event_aturi, e);
204204+ Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to check bookmark status".to_string()).into_response())
90205 }
91206 }
92207}
···97212 HxBoosted(hx_boosted): HxBoosted,
98213 Query(params): Query<CalendarViewParams>,
99214) -> Result<impl IntoResponse, WebError> {
215215+ tracing::info!("Starting handle_bookmark_calendar function");
100216 let current_handle = ctx.auth.require(&ctx.web_context.config.destination_key, "/bookmarks")?;
101217102218 let view_mode = params.view.as_deref().unwrap_or("timeline");
···133249 .await
134250 {
135251 Ok(paginated_events) => {
252252+ // Store count before moving events
253253+ let events_count = paginated_events.events.len();
254254+255255+ // Determine timezone from current user, fallback to UTC
256256+ let tz = current_handle.tz.parse::<Tz>().unwrap_or(Tz::UTC);
257257+258258+ // Process events to include timezone-aware date formatting
259259+ let processed_events = process_bookmarked_events_with_timezone(
260260+ paginated_events.events,
261261+ &tz,
262262+ Some(&ctx.language.0)
263263+ );
264264+136265 let template_name = match view_mode {
137266 "calendar" => "bookmark_calendar_grid",
138267 _ => "bookmark_calendar_timeline",
139268 };
140269270270+ // Get current date for calendar context
271271+ let now = chrono::Utc::now();
272272+ let month_names = vec![
273273+ "January", "February", "March", "April", "May", "June",
274274+ "July", "August", "September", "October", "November", "December"
275275+ ];
276276+141277 let template_context = template_context! {
142142- events => paginated_events.events,
278278+ events => processed_events,
143279 total_count => paginated_events.total_count,
144280 has_more => paginated_events.has_more,
145281 current_offset => offset,
146282 view_mode => view_mode,
147283 filter_tags => params.tags.unwrap_or_default(),
284284+ month => now.month(),
285285+ year => now.year(),
286286+ month_names => month_names,
148287 };
149288150150- let html = renderer.render_template(
289289+ tracing::info!(
290290+ template_name = %template_name,
291291+ events_count = events_count,
292292+ total_count = paginated_events.total_count,
293293+ view_mode = %view_mode,
294294+ "About to render bookmark events template"
295295+ );
296296+297297+ let response = renderer.render_template(
151298 template_name,
152299 template_context,
153300 ctx.current_handle.as_ref(),
154301 "/bookmarks"
155302 );
156156- Ok(Html(html).into_response())
303303+304304+ tracing::info!("Successfully rendered bookmark events template");
305305+ tracing::info!("Response status: {:?}", response.status());
306306+ let result = Ok(response);
307307+ tracing::info!("About to return from handle_bookmark_calendar function");
308308+ result
157309 }
158310 Err(e) => {
159311 error!("Failed to get bookmarked events for user {}: {}", current_handle.did, e);
160160- Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to load bookmarks".to_string()).into_response())
312312+ let error_message = LOCALES.lookup(&ctx.language.0, "error-occurred");
313313+ Ok((StatusCode::INTERNAL_SERVER_ERROR, error_message).into_response())
161314 }
162315 }
163316}
···165318/// Handle removing a bookmark
166319pub async fn handle_remove_bookmark(
167320 ctx: UserRequestContext,
168168- Path(bookmark_aturi): Path<String>,
321321+ Path(event_aturi): Path<String>,
169322) -> Result<impl IntoResponse, WebError> {
170323 let current_handle = ctx.auth.require(&ctx.web_context.config.destination_key, "/bookmarks")?;
171324172172- // Decode the bookmark AT-URI if URL-encoded
173173- let bookmark_aturi = urlencoding::decode(&bookmark_aturi)
174174- .map_err(|_| anyhow::anyhow!("Invalid bookmark URI"))?
325325+ // Decode the event AT-URI if URL-encoded
326326+ let event_aturi = urlencoding::decode(&event_aturi)
327327+ .map_err(|_| anyhow::anyhow!("Invalid event URI"))?
175328 .to_string();
329329+330330+ // Add back the at:// prefix since it was stripped in the JavaScript
331331+ let full_event_aturi = format!("at://{}", event_aturi);
176332177333 let bookmark_service = EventBookmarkService::new(
178334 Arc::new(ctx.web_context.pool.clone()),
179335 Arc::new(ctx.web_context.atrium_oauth_manager.clone()),
180336 );
181337338338+ // Get session group from the Auth context
339339+ let session_group = ctx.auth.1.as_ref()
340340+ .map(|oauth_session| oauth_session.session_group.as_str())
341341+ .unwrap_or("unknown"); // Fallback if no session
342342+182343 match bookmark_service
183183- .remove_bookmark(¤t_handle.did, &bookmark_aturi)
344344+ .remove_bookmark(¤t_handle.did, &full_event_aturi, session_group)
184345 .await
185346 {
186347 Ok(()) => {
187187- info!("Successfully removed bookmark {} for user {}", bookmark_aturi, current_handle.did);
348348+ info!("Successfully removed bookmark {} for user {}", full_event_aturi, current_handle.did);
188349189189- // Return empty HTML for HTMX to remove the element
190190- Ok(Html(String::new()).into_response())
350350+ // Return HX-Refresh header to reload the page and show the original bookmark button from view_event template
351351+ Ok((StatusCode::OK, [("HX-Refresh", "true")], "").into_response())
191352 }
192353 Err(e) => {
193193- error!("Failed to remove bookmark {}: {}", bookmark_aturi, e);
194194- Ok((StatusCode::INTERNAL_SERVER_ERROR, "Failed to remove bookmark".to_string()).into_response())
354354+ error!("Failed to remove bookmark {}: {}", full_event_aturi, e);
355355+ let error_message = LOCALES.lookup(&ctx.language.0, "error-occurred");
356356+ Ok((StatusCode::INTERNAL_SERVER_ERROR, error_message).into_response())
195357 }
196358 }
197359}
···286448}
287449288450/// Generate iCal content from bookmarked events
289289-fn generate_ical_from_bookmarks(bookmarks: &[crate::storage::event_bookmarks::EventBookmark]) -> String {
451451+fn generate_ical_from_bookmarks(bookmarked_events: &[crate::storage::event_bookmarks::BookmarkedEvent]) -> String {
290452 let mut ical = String::from("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//smokesignal//EN\r\n");
291453292292- for bookmark in bookmarks {
454454+ for bookmarked_event in bookmarked_events {
455455+ // Extract start date from the record JSON if available
456456+ let start_date = bookmarked_event.event.record
457457+ .get("start_date")
458458+ .and_then(|v| v.as_str())
459459+ .unwrap_or("20240101T000000Z");
460460+293461 ical.push_str(&format!(
294294- "BEGIN:VEVENT\r\nUID:{}\r\nSUMMARY:Bookmarked Event\r\nDTSTAMP:{}\r\nEND:VEVENT\r\n",
295295- bookmark.id,
296296- bookmark.created_at.format("%Y%m%dT%H%M%SZ")
462462+ "BEGIN:VEVENT\r\nUID:{}\r\nSUMMARY:{}\r\nDTSTAMP:{}\r\nDTSTART:{}\r\nEND:VEVENT\r\n",
463463+ bookmarked_event.bookmark.id,
464464+ bookmarked_event.event.name,
465465+ bookmarked_event.bookmark.created_at.format("%Y%m%dT%H%M%SZ"),
466466+ start_date
297467 ));
298468 }
299469300470 ical.push_str("END:VCALENDAR\r\n");
301471 ical
302472}
473473+474474+#[derive(Deserialize)]
475475+pub struct BookmarkModalParams {
476476+ event_aturi: String,
477477+}
478478+479479+/// Handle showing the bookmark modal
480480+pub async fn handle_bookmark_modal(
481481+ ctx: UserRequestContext,
482482+ Query(params): Query<BookmarkModalParams>,
483483+) -> Result<impl IntoResponse, WebError> {
484484+ let current_handle = ctx.auth.require(&ctx.web_context.config.destination_key, "/bookmarks/modal")?;
485485+486486+ // Check if the event is already bookmarked
487487+ let bookmark_service = EventBookmarkService::new(
488488+ Arc::new(ctx.web_context.pool.clone()),
489489+ Arc::new(ctx.web_context.atrium_oauth_manager.clone()),
490490+ );
491491+492492+ let existing_bookmark = match bookmark_service
493493+ .get_bookmark(¤t_handle.did, ¶ms.event_aturi)
494494+ .await
495495+ {
496496+ Ok(bookmark) => bookmark,
497497+ Err(e) => {
498498+ error!("Failed to check bookmark status: {}", e);
499499+ None
500500+ }
501501+ };
502502+503503+ // Create the template renderer with HTMX request detection
504504+ let language_clone = ctx.language.clone();
505505+ let renderer = create_renderer!(ctx.web_context.clone(), language_clone, true, true); // hx_request = true
506506+507507+ Ok(renderer.render_template(
508508+ "bookmark_modal",
509509+ template_context! {
510510+ event_aturi => params.event_aturi,
511511+ existing_bookmark => existing_bookmark,
512512+ is_already_bookmarked => existing_bookmark.is_some(),
513513+ },
514514+ Some(¤t_handle),
515515+ "/bookmarks/modal",
516516+ ))
517517+}