Skip to main content

max / makenotwork

3.7 KB · 123 lines History Blame Raw
1 //! Creator waitlist application.
2
3 use axum::{
4 Form,
5 extract::State,
6 http::{StatusCode, header::HeaderMap},
7 response::{IntoResponse, Response},
8 };
9 use serde::Deserialize;
10 use std::fmt::Write as _;
11
12 use sqlx::PgPool;
13
14 use crate::{
15 auth::AuthUser,
16 db,
17 error::{AppError, Result},
18 helpers::is_htmx_request,
19 templates::AlertTemplate,
20 validation,
21 };
22
23 /// Form input for applying to the creator waitlist.
24 #[derive(Debug, Deserialize)]
25 pub(crate) struct WaitlistApplyForm {
26 pub pitch: String,
27 /// Preferred creator tier (basic, small_files, big_files, everything).
28 #[serde(default)]
29 pub preferred_tier: Option<String>,
30 /// Whether the applicant is requesting a free trial.
31 #[serde(default)]
32 pub free_trial: Option<String>,
33 /// Requested trial length (e.g. "2 weeks", "1 month").
34 #[serde(default)]
35 pub trial_length: Option<String>,
36 /// What the applicant wants to test during the trial.
37 #[serde(default)]
38 pub trial_reason: Option<String>,
39 }
40
41 /// Submit an application to the creator waitlist.
42 #[tracing::instrument(skip_all, name = "users::waitlist_apply")]
43 pub(in crate::routes::api) async fn waitlist_apply(
44 State(db): State<PgPool>,
45 headers: HeaderMap,
46 AuthUser(user): AuthUser,
47 Form(form): Form<WaitlistApplyForm>,
48 ) -> Result<Response> {
49 let is_htmx = is_htmx_request(&headers);
50
51 // Check if user already has creator access
52 if user.can_create_projects {
53 if is_htmx {
54 return Ok(
55 AlertTemplate::new("info", "You already have creator access.").into_response(),
56 );
57 }
58 return Err(AppError::BadRequest("Already a creator".to_string()));
59 }
60
61 // Check verified email
62 let db_user = db::users::get_user_by_id(&db, user.id)
63 .await?
64 .ok_or(AppError::NotFound)?;
65
66 if !db_user.email_verified {
67 if is_htmx {
68 return Ok(
69 AlertTemplate::new("error", "Please verify your email first.").into_response(),
70 );
71 }
72 return Err(AppError::BadRequest("Email not verified".to_string()));
73 }
74
75 // Check if already applied
76 if db::waitlist::get_waitlist_entry_by_user(&db, user.id)
77 .await?
78 .is_some()
79 {
80 if is_htmx {
81 return Ok(AlertTemplate::new("info", "You've already applied.").into_response());
82 }
83 return Err(AppError::BadRequest("Already applied".to_string()));
84 }
85
86 // Validate pitch
87 let pitch = form.pitch.trim().to_string();
88 validation::validate_waitlist_pitch(&pitch)?;
89
90 // Build the full application text with optional fields appended.
91 let mut full_pitch = pitch;
92 if let Some(tier) = form.preferred_tier.as_deref().filter(|s| !s.is_empty()) {
93 write!(full_pitch, "\n\n[Preferred tier: {tier}]").unwrap();
94 }
95 if form.free_trial.as_deref() == Some("yes") {
96 let length = form
97 .trial_length
98 .as_deref()
99 .unwrap_or("not specified")
100 .trim();
101 let reason = form.trial_reason.as_deref().unwrap_or("").trim();
102 write!(full_pitch, "\n\n[Free trial requested: {length}]").unwrap();
103 if !reason.is_empty() {
104 write!(full_pitch, "\n[Trial reason: {reason}]").unwrap();
105 }
106 }
107
108 db::waitlist::create_waitlist_entry(&db, user.id, &full_pitch).await?;
109
110 tracing::info!(user_id = %user.id, "waitlist application submitted");
111
112 if is_htmx {
113 // Return a success message that replaces the form
114 return Ok(AlertTemplate::new(
115 "success",
116 "Application submitted. We'll review it and let you know.",
117 )
118 .into_response());
119 }
120
121 Ok(StatusCode::NO_CONTENT.into_response())
122 }
123