From 1c66a85e750dfc39c0622d8304a8fb375eaaa036 Mon Sep 17 00:00:00 2001 From: Sam Wlody Date: Mon, 6 Oct 2025 01:33:29 -0400 Subject: [PATCH 1/2] initial external event attempt --- ...bd1b8881297a06d0e0d333b065aec5d9da24e.json | 6 ++ ...b06361241fa74141815839b5dbc3d9d1fe699.json | 6 ++ ...6864f0ce11920d5926a5c87b16e94dbc3c116.json | 6 ++ ...c41d13da40b3e927ab0eca381ab3af2b81455.json | 6 ++ ...d188e9e2b3dbc7b951ff9c00db06be355c7c9.json | 6 ++ frontend/templates/events/edit.html | 80 ++++++++++++++++++- .../0028_add_external_events_fields.down.sql | 2 + .../0028_add_external_events_fields.up.sql | 1 + src/app/events.rs | 15 +++- src/db/event.rs | 2 + 10 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 migrations/0028_add_external_events_fields.down.sql create mode 100644 migrations/0028_add_external_events_fields.up.sql diff --git a/.sqlx/query-0d4d698b039ac95743392f44379bd1b8881297a06d0e0d333b065aec5d9da24e.json b/.sqlx/query-0d4d698b039ac95743392f44379bd1b8881297a06d0e0d333b065aec5d9da24e.json index da07638..9bf6d23 100644 --- a/.sqlx/query-0d4d698b039ac95743392f44379bd1b8881297a06d0e0d333b065aec5d9da24e.json +++ b/.sqlx/query-0d4d698b039ac95743392f44379bd1b8881297a06d0e0d333b065aec5d9da24e.json @@ -57,6 +57,11 @@ "name": "guest_list_id", "ordinal": 10, "type_info": "Integer" + }, + { + "name": "external_event_url", + "ordinal": 11, + "type_info": "Text" } ], "parameters": { @@ -73,6 +78,7 @@ false, false, false, + true, true ] }, diff --git a/.sqlx/query-4ea1d3b99a015fc812c7e21117eb06361241fa74141815839b5dbc3d9d1fe699.json b/.sqlx/query-4ea1d3b99a015fc812c7e21117eb06361241fa74141815839b5dbc3d9d1fe699.json index 9df84a1..952df0d 100644 --- a/.sqlx/query-4ea1d3b99a015fc812c7e21117eb06361241fa74141815839b5dbc3d9d1fe699.json +++ b/.sqlx/query-4ea1d3b99a015fc812c7e21117eb06361241fa74141815839b5dbc3d9d1fe699.json @@ -57,6 +57,11 @@ "name": "guest_list_id", "ordinal": 10, "type_info": "Integer" + }, + { + "name": "external_event_url", + "ordinal": 11, + "type_info": "Text" } ], "parameters": { @@ -73,6 +78,7 @@ false, false, false, + true, true ] }, diff --git a/.sqlx/query-71a23021623175a1fc41826a3fa6864f0ce11920d5926a5c87b16e94dbc3c116.json b/.sqlx/query-71a23021623175a1fc41826a3fa6864f0ce11920d5926a5c87b16e94dbc3c116.json index 5e021e3..20c1fb0 100644 --- a/.sqlx/query-71a23021623175a1fc41826a3fa6864f0ce11920d5926a5c87b16e94dbc3c116.json +++ b/.sqlx/query-71a23021623175a1fc41826a3fa6864f0ce11920d5926a5c87b16e94dbc3c116.json @@ -57,6 +57,11 @@ "name": "guest_list_id", "ordinal": 10, "type_info": "Integer" + }, + { + "name": "external_event_url", + "ordinal": 11, + "type_info": "Text" } ], "parameters": { @@ -73,6 +78,7 @@ false, false, false, + true, true ] }, diff --git a/.sqlx/query-9546f551138edd30d2c27560d55c41d13da40b3e927ab0eca381ab3af2b81455.json b/.sqlx/query-9546f551138edd30d2c27560d55c41d13da40b3e927ab0eca381ab3af2b81455.json index 144ca32..f215fba 100644 --- a/.sqlx/query-9546f551138edd30d2c27560d55c41d13da40b3e927ab0eca381ab3af2b81455.json +++ b/.sqlx/query-9546f551138edd30d2c27560d55c41d13da40b3e927ab0eca381ab3af2b81455.json @@ -57,6 +57,11 @@ "name": "guest_list_id", "ordinal": 10, "type_info": "Integer" + }, + { + "name": "external_event_url", + "ordinal": 11, + "type_info": "Text" } ], "parameters": { @@ -73,6 +78,7 @@ false, false, false, + true, true ] }, diff --git a/.sqlx/query-fb9d4adb6c2f81ceb869c34fd26d188e9e2b3dbc7b951ff9c00db06be355c7c9.json b/.sqlx/query-fb9d4adb6c2f81ceb869c34fd26d188e9e2b3dbc7b951ff9c00db06be355c7c9.json index f3de7fe..55105d8 100644 --- a/.sqlx/query-fb9d4adb6c2f81ceb869c34fd26d188e9e2b3dbc7b951ff9c00db06be355c7c9.json +++ b/.sqlx/query-fb9d4adb6c2f81ceb869c34fd26d188e9e2b3dbc7b951ff9c00db06be355c7c9.json @@ -57,6 +57,11 @@ "name": "guest_list_id", "ordinal": 10, "type_info": "Integer" + }, + { + "name": "external_event_url", + "ordinal": 11, + "type_info": "Text" } ], "parameters": { @@ -73,6 +78,7 @@ false, false, false, + true, true ] }, diff --git a/frontend/templates/events/edit.html b/frontend/templates/events/edit.html index cbc6196..817704f 100644 --- a/frontend/templates/events/edit.html +++ b/frontend/templates/events/edit.html @@ -7,6 +7,16 @@

Edit Event

+
+ +
Edit Event /> +
+ + +
+
@@ -143,11 +165,26 @@

Available Spots

list: $("spots"), add: $("add"), spots: () => [...ui.list.querySelectorAll(".spot")], + externalEvent: $("external-event"), flyer: { input: $("flyer"), text: $("flyer-text"), filename: $("flyer-filename"), preview: $("flyer-preview"), + field: document.querySelector(".field.flyer"), + }, + fields: { + externalUrl: document + .querySelector('input[name="external_event_url"]') + .closest(".field"), + description: document + .querySelector('input[name="description"]') + .closest(".field"), + capacity: document + .querySelector('input[name="capacity"]') + .closest(".field"), + end: document.querySelector('input[name="end"]').closest(".field"), + spots: $("spots"), }, }; const KINDS = { @@ -164,6 +201,42 @@

Available Spots

required_notice_hours: "work", }; + function toggleInternalExternal() { + const isExternal = ui.externalEvent.checked; + + ui.fields.externalUrl.hidden = !isExternal; + const externalUrlInput = ui.fields.externalUrl.querySelector( + 'input[name="external_event_url"]', + ); + externalUrlInput.required = isExternal; + + ui.flyer.field.hidden = isExternal; + + ui.fields.description.hidden = isExternal; + const descriptionInput = ui.fields.description.querySelector( + 'input[name="description"]', + ); + descriptionInput.required = !isExternal; + + ui.fields.capacity.hidden = isExternal; + const capacityInput = ui.fields.capacity.querySelector( + 'input[name="capacity"]', + ); + capacityInput.required = !isExternal; + + ui.fields.end.hidden = isExternal; + + ui.fields.spots.hidden = isExternal; + } + + /* ---------- External Event Toggle ----------------------------------------- */ + ui.externalEvent.addEventListener("change", () => { + toggleInternalExternal(); + }); + + // Set initial state based on checkbox + toggleInternalExternal(); + /* ---------- Rendering ----------------------------------------------------- */ const template = (t) => `
@@ -273,7 +346,11 @@

Available Spots

/* ---------- Form submit handler ------------------------------------------- */ ui.form.addEventListener("submit", async (e) => { e.preventDefault(); - if (ui.spots().length == 0) { + + const isExternal = ui.externalEvent.checked; + + // Only check for spots if it's not an external event + if (!isExternal && ui.spots().length == 0) { alert("Add at least one reservation!"); return; } @@ -330,6 +407,7 @@

Available Spots

capacity: value("capacity"), unlisted: value("unlisted"), guest_list_id: value("guest_list_id"), + external_event_url: isExternal ? value("external_event_url") : null, spots: ui.spots().map((t, i) => { const kind = value("kind", t); const spot = { diff --git a/migrations/0028_add_external_events_fields.down.sql b/migrations/0028_add_external_events_fields.down.sql new file mode 100644 index 0000000..125ad8b --- /dev/null +++ b/migrations/0028_add_external_events_fields.down.sql @@ -0,0 +1,2 @@ +DELETE FROM events WHERE external_event_url IS NOT NULL; +ALTER TABLE events DROP COLUMN external_event_url; \ No newline at end of file diff --git a/migrations/0028_add_external_events_fields.up.sql b/migrations/0028_add_external_events_fields.up.sql new file mode 100644 index 0000000..8bf9dd7 --- /dev/null +++ b/migrations/0028_add_external_events_fields.up.sql @@ -0,0 +1 @@ +ALTER TABLE events ADD COLUMN external_event_url TEXT; \ No newline at end of file diff --git a/src/app/events.rs b/src/app/events.rs index ad70d58..490d14a 100644 --- a/src/app/events.rs +++ b/src/app/events.rs @@ -38,7 +38,7 @@ mod read { user: Option, State(state): State, Path(slug): Path, - ) -> AppResult { + ) -> AppResult { #[derive(Template, WebTemplate)] #[template(path = "events/view.html")] struct Html { @@ -47,8 +47,15 @@ mod read { flyer: Option, } let event = Event::lookup_by_slug(&state.db, &slug).await?.ok_or(AppError::NotFound)?; - let flyer = EventFlyer::lookup(&state.db, event.id).await?; - Ok(Html { user, event, flyer }) + + if let Some(external_url) = &event.external_event_url + && !external_url.is_empty() + { + return Ok(Redirect::to(external_url).into_response()); + } else { + let flyer = EventFlyer::lookup(&state.db, event.id).await?; + Ok(Html { user, event, flyer }.into_response()) + } } // List all events. @@ -128,6 +135,8 @@ mod edit { unlisted: false, guest_list_id: None, + external_event_url: None, + created_at: Utc::now().naive_utc(), updated_at: Utc::now().naive_utc(), }, diff --git a/src/db/event.rs b/src/db/event.rs index a87c28d..78ff042 100644 --- a/src/db/event.rs +++ b/src/db/event.rs @@ -18,6 +18,7 @@ pub struct Event { pub capacity: i64, pub unlisted: bool, pub guest_list_id: Option, + pub external_event_url: Option, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, @@ -35,6 +36,7 @@ pub struct UpdateEvent { pub capacity: i64, pub unlisted: bool, pub guest_list_id: Option, + pub external_event_url: Option, } #[derive(Debug, serde::Serialize)] From 2900d05bac2a7d2f623fbd7b0e556686e3767c90 Mon Sep 17 00:00:00 2001 From: Sam Wlody Date: Mon, 13 Oct 2025 18:54:22 -0400 Subject: [PATCH 2/2] failure --- ...85b30b52ec6c32198e7af6ef1c2bf5b69ba6.json} | 6 +- Cargo.toml | 2 +- frontend/templates/events/edit.html | 264 ++++++++++++------ src/app/events.rs | 8 +- src/db/event.rs | 4 +- src/utils/error.rs | 5 +- 6 files changed, 195 insertions(+), 94 deletions(-) rename .sqlx/{query-d7cc388ea81b0e270c94c3cb8399fe69f0cc31f92c3dfe7f637b20f023dcb1cb.json => query-499f1423909a4331b5770a31452785b30b52ec6c32198e7af6ef1c2bf5b69ba6.json} (63%) diff --git a/.sqlx/query-d7cc388ea81b0e270c94c3cb8399fe69f0cc31f92c3dfe7f637b20f023dcb1cb.json b/.sqlx/query-499f1423909a4331b5770a31452785b30b52ec6c32198e7af6ef1c2bf5b69ba6.json similarity index 63% rename from .sqlx/query-d7cc388ea81b0e270c94c3cb8399fe69f0cc31f92c3dfe7f637b20f023dcb1cb.json rename to .sqlx/query-499f1423909a4331b5770a31452785b30b52ec6c32198e7af6ef1c2bf5b69ba6.json index 63ac512..c0b5d55 100644 --- a/.sqlx/query-d7cc388ea81b0e270c94c3cb8399fe69f0cc31f92c3dfe7f637b20f023dcb1cb.json +++ b/.sqlx/query-499f1423909a4331b5770a31452785b30b52ec6c32198e7af6ef1c2bf5b69ba6.json @@ -1,12 +1,12 @@ { "db_name": "SQLite", - "query": "UPDATE events\n SET title = ?,\n slug = ?,\n description = ?,\n start = ?,\n end = ?,\n capacity = ?,\n unlisted = ?,\n guest_list_id = ?\n WHERE id = ?", + "query": "UPDATE events\n SET title = ?,\n slug = ?,\n description = ?,\n start = ?,\n end = ?,\n capacity = ?,\n unlisted = ?,\n guest_list_id = ?,\n external_event_url = ?\n WHERE id = ?", "describe": { "columns": [], "parameters": { - "Right": 9 + "Right": 10 }, "nullable": [] }, - "hash": "d7cc388ea81b0e270c94c3cb8399fe69f0cc31f92c3dfe7f637b20f023dcb1cb" + "hash": "499f1423909a4331b5770a31452785b30b52ec6c32198e7af6ef1c2bf5b69ba6" } diff --git a/Cargo.toml b/Cargo.toml index a71db1c..f3e33b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] -axum = { version = "0.8", default-features = false, features = ["http2", "query", "form", "json", "multipart"] } +axum = { version = "0.8", default-features = false, features = ["http2", "query", "form", "json", "multipart", "tracing"] } axum-server = { version = "0.7", features = ["tls-rustls"] } axum-extra = { version = "0.10", features = ["cookie"] } askama = { version = "0.13", features = ["serde_json"] } diff --git a/frontend/templates/events/edit.html b/frontend/templates/events/edit.html index 817704f..cd8d1a7 100644 --- a/frontend/templates/events/edit.html +++ b/frontend/templates/events/edit.html @@ -8,17 +8,17 @@

Edit Event

-
Edit Event />
-
- - -
-
@@ -152,6 +140,122 @@

Available Spots

+ + + + + +
+
+ + +
+ +
+ + +
+ +
+ +
+ +
+
+ No flyer chosen + +
+ Choose File +
+
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + + + + @@ -161,11 +265,12 @@

Available Spots

/* ---------- DOM ----------------------------------------------------------- */ const $ = (id) => document.getElementById(id); const ui = { - form: $("form"), + internalEventForm: $("internal-event-form"), + externalEventForm: $("external-event-form"), list: $("spots"), add: $("add"), spots: () => [...ui.list.querySelectorAll(".spot")], - externalEvent: $("external-event"), + externalEventCheckBox: $("external-event-checkbox"), flyer: { input: $("flyer"), text: $("flyer-text"), @@ -173,19 +278,6 @@

Available Spots

preview: $("flyer-preview"), field: document.querySelector(".field.flyer"), }, - fields: { - externalUrl: document - .querySelector('input[name="external_event_url"]') - .closest(".field"), - description: document - .querySelector('input[name="description"]') - .closest(".field"), - capacity: document - .querySelector('input[name="capacity"]') - .closest(".field"), - end: document.querySelector('input[name="end"]').closest(".field"), - spots: $("spots"), - }, }; const KINDS = { free: "Free", @@ -202,35 +294,14 @@

Available Spots

}; function toggleInternalExternal() { - const isExternal = ui.externalEvent.checked; - - ui.fields.externalUrl.hidden = !isExternal; - const externalUrlInput = ui.fields.externalUrl.querySelector( - 'input[name="external_event_url"]', - ); - externalUrlInput.required = isExternal; - - ui.flyer.field.hidden = isExternal; - - ui.fields.description.hidden = isExternal; - const descriptionInput = ui.fields.description.querySelector( - 'input[name="description"]', - ); - descriptionInput.required = !isExternal; - - ui.fields.capacity.hidden = isExternal; - const capacityInput = ui.fields.capacity.querySelector( - 'input[name="capacity"]', - ); - capacityInput.required = !isExternal; + const isExternal = ui.externalEventCheckBox.checked; - ui.fields.end.hidden = isExternal; - - ui.fields.spots.hidden = isExternal; + ui.internalEventForm.style.display = isExternal ? "none" : "block"; + ui.externalEventForm.style.display = isExternal ? "block" : "none"; } /* ---------- External Event Toggle ----------------------------------------- */ - ui.externalEvent.addEventListener("change", () => { + ui.externalEventCheckBox.addEventListener("change", () => { toggleInternalExternal(); }); @@ -344,25 +415,33 @@

Available Spots

updateFlyerPreview(); /* ---------- Form submit handler ------------------------------------------- */ - ui.form.addEventListener("submit", async (e) => { + const handleFormSubmit = async (e) => { e.preventDefault(); - const isExternal = ui.externalEvent.checked; + const isExternal = ui.externalEventCheckBox.checked; + const activeForm = isExternal + ? ui.externalEventForm + : ui.internalEventForm; - // Only check for spots if it's not an external event if (!isExternal && ui.spots().length == 0) { alert("Add at least one reservation!"); return; } // Retrieve the value of an input, with adjustments for HTML input type weirdness - let value = (name, root = ui.form) => { + let value = (name, root = activeForm) => { const el = root.querySelector(`[name="${name}"]`); + if (!el) { + console.warn(`Element with name "${name}" not found in form`); + return null; + } const type = el.dataset?.type || el.type; if (type == "number") { return el.value == "" ? null : +el.value; } else if (type == "checkbox") { return el.checked; + } else if (type == "boolean") { + return el.value === "true"; } else { return el.value == "" ? null : el.value; } @@ -397,41 +476,48 @@

Available Spots

return [start, end]; })(); + console.log(value("external_event_url")); + const body = { id: parseInt("{{ event.id }}") || undefined, title: value("title"), slug: value("slug"), - description: value("description"), + description: value("description") || "", start, end, - capacity: value("capacity"), + capacity: value("capacity") || 0, unlisted: value("unlisted"), guest_list_id: value("guest_list_id"), - external_event_url: isExternal ? value("external_event_url") : null, - spots: ui.spots().map((t, i) => { - const kind = value("kind", t); - const spot = { - id: value("id", t), - name: value("name", t), - description: value("description", t), - qty_total: value("qty_total", t), - qty_per_person: value("qty_per_person", t), - kind, - sort: i, - }; - - if (kind == "fixed") { - spot.required_contribution = value("required_contribution", t); - } else if (kind == "variable") { - spot.min_contribution = value("min_contribution", t); - spot.max_contribution = value("max_contribution", t); - spot.suggested_contribution = value("suggested_contribution", t); - } else if (kind == "work") { - spot.required_notice_hours = value("required_notice_hours", t); - } - - return spot; - }), + external_event_url: value("external_event_url"), + spots: isExternal + ? [] + : ui.spots().map((t, i) => { + const kind = value("kind", t); + const spot = { + id: value("id", t), + name: value("name", t), + description: value("description", t), + qty_total: value("qty_total", t), + qty_per_person: value("qty_per_person", t), + kind, + sort: i, + }; + + if (kind == "fixed") { + spot.required_contribution = value("required_contribution", t); + } else if (kind == "variable") { + spot.min_contribution = value("min_contribution", t); + spot.max_contribution = value("max_contribution", t); + spot.suggested_contribution = value( + "suggested_contribution", + t, + ); + } else if (kind == "work") { + spot.required_notice_hours = value("required_notice_hours", t); + } + + return spot; + }), }; const slug = value("slug"); @@ -457,7 +543,11 @@

Available Spots

} catch (e) { alert(`Error: ${e}`); } - }); + }; + + // Apply the submit handler to both forms + ui.internalEventForm.addEventListener("submit", handleFormSubmit); + ui.externalEventForm.addEventListener("submit", handleFormSubmit); // Populate existing spots from the backend let spots = JSON.parse(`{{ spots | json | safe }}`); diff --git a/src/app/events.rs b/src/app/events.rs index 490d14a..9f4a0c4 100644 --- a/src/app/events.rs +++ b/src/app/events.rs @@ -48,6 +48,8 @@ mod read { } let event = Event::lookup_by_slug(&state.db, &slug).await?.ok_or(AppError::NotFound)?; + tracing::info!("event view: {:?}", event); + if let Some(external_url) = &event.external_event_url && !external_url.is_empty() { @@ -178,7 +180,11 @@ mod edit { match field.name().unwrap_or("") { "data" => { let text = field.text().await?; - form = Some(serde_json::from_str(&text).map_err(|_| AppError::BadRequest)?); + let res = serde_json::from_str(&text); + if let Err(e) = res.as_ref() { + tracing::error!("form parse error: {text} -> {e}"); + } + form = Some(res.map_err(|_| AppError::BadRequest)?); } "flyer" => { let data = field.bytes().await?; diff --git a/src/db/event.rs b/src/db/event.rs index 78ff042..1b391af 100644 --- a/src/db/event.rs +++ b/src/db/event.rs @@ -130,7 +130,8 @@ impl Event { end = ?, capacity = ?, unlisted = ?, - guest_list_id = ? + guest_list_id = ?, + external_event_url = ? WHERE id = ?"#, event.title, event.slug, @@ -140,6 +141,7 @@ impl Event { event.capacity, event.unlisted, event.guest_list_id, + event.external_event_url, id ) .execute(db) diff --git a/src/utils/error.rs b/src/utils/error.rs index 471614f..ce784e1 100644 --- a/src/utils/error.rs +++ b/src/utils/error.rs @@ -47,7 +47,10 @@ impl IntoResponse for AppError { let (status, message) = match self { AppError::BadRequest => error_400(), - AppError::BadMultipart(_) => error_400(), + AppError::BadMultipart(e) => { + tracing::error!("multipart error: {e}"); + error_400() + }, AppError::Unauthorized => error_401(), AppError::NotFound => error_404(), AppError::Database(_) => error_500(),