//! Creator waitlist application. use axum::{ extract::State, http::{header::HeaderMap, StatusCode}, response::{IntoResponse, Response}, Form, }; use serde::Deserialize; use crate::{ auth::AuthUser, db, error::{AppError, Result}, helpers::is_htmx_request, templates::AlertTemplate, validation, AppState, }; /// Form input for applying to the creator waitlist. #[derive(Debug, Deserialize)] pub struct WaitlistApplyForm { pub pitch: String, /// Preferred creator tier (basic, small_files, big_files, everything). #[serde(default)] pub preferred_tier: Option, /// Whether the applicant is requesting a free trial. #[serde(default)] pub free_trial: Option, /// Requested trial length (e.g. "2 weeks", "1 month"). #[serde(default)] pub trial_length: Option, /// What the applicant wants to test during the trial. #[serde(default)] pub trial_reason: Option, } /// Submit an application to the creator waitlist. #[tracing::instrument(skip_all, name = "users::waitlist_apply")] pub(in crate::routes::api) async fn waitlist_apply( State(state): State, headers: HeaderMap, AuthUser(user): AuthUser, Form(form): Form, ) -> Result { let is_htmx = is_htmx_request(&headers); // Check if user already has creator access if user.can_create_projects { if is_htmx { return Ok(AlertTemplate::new("info", "You already have creator access.").into_response()); } return Err(AppError::BadRequest("Already a creator".to_string())); } // Check verified email let db_user = db::users::get_user_by_id(&state.db, user.id) .await? .ok_or(AppError::NotFound)?; if !db_user.email_verified { if is_htmx { return Ok(AlertTemplate::new("error", "Please verify your email first.").into_response()); } return Err(AppError::BadRequest("Email not verified".to_string())); } // Check if already applied if db::waitlist::get_waitlist_entry_by_user(&state.db, user.id).await?.is_some() { if is_htmx { return Ok(AlertTemplate::new("info", "You've already applied.").into_response()); } return Err(AppError::BadRequest("Already applied".to_string())); } // Validate pitch let pitch = form.pitch.trim().to_string(); validation::validate_waitlist_pitch(&pitch)?; // Build the full application text with optional fields appended. let mut full_pitch = pitch; if let Some(tier) = form.preferred_tier.as_deref().filter(|s| !s.is_empty()) { full_pitch.push_str(&format!("\n\n[Preferred tier: {}]", tier)); } if form.free_trial.as_deref() == Some("yes") { let length = form.trial_length.as_deref().unwrap_or("not specified").trim(); let reason = form.trial_reason.as_deref().unwrap_or("").trim(); full_pitch.push_str(&format!("\n\n[Free trial requested: {}]", length)); if !reason.is_empty() { full_pitch.push_str(&format!("\n[Trial reason: {}]", reason)); } } // Create entry db::waitlist::create_waitlist_entry(&state.db, user.id, &full_pitch).await?; tracing::info!(user_id = %user.id, "waitlist application submitted"); if is_htmx { // Return a success message that replaces the form return Ok(AlertTemplate::new("success", "Application submitted. We'll review it and let you know.").into_response()); } Ok(StatusCode::NO_CONTENT.into_response()) }