//! Creator waitlist application. use axum::{ Form, extract::State, http::{StatusCode, header::HeaderMap}, response::{IntoResponse, Response}, }; use serde::Deserialize; use std::fmt::Write as _; use sqlx::PgPool; use crate::{ auth::AuthUser, db, error::{AppError, Result}, helpers::is_htmx_request, templates::AlertTemplate, validation, }; /// Form input for applying to the creator waitlist. #[derive(Debug, Deserialize)] pub(crate) 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(db): 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(&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(&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()) { write!(full_pitch, "\n\n[Preferred tier: {tier}]").unwrap(); } 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(); write!(full_pitch, "\n\n[Free trial requested: {length}]").unwrap(); if !reason.is_empty() { write!(full_pitch, "\n[Trial reason: {reason}]").unwrap(); } } db::waitlist::create_waitlist_entry(&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()) }