Skip to main content

max / makenotwork

6.5 KB · 187 lines History Blame Raw
1 //! Step rendering dispatcher for the item wizard.
2
3 use axum::response::{IntoResponse, Response};
4 use tower_sessions::Session;
5
6 use crate::{
7 db::{self, ItemType, ProjectFeature},
8 error::{AppError, Result},
9 helpers::get_csrf_token,
10 templates::{
11 BundleableItem, WizardItemBasicsTemplate, WizardItemContentTemplate,
12 WizardItemPreviewTemplate, WizardItemPricingTemplate, WizardItemSectionsTemplate,
13 WizardItemTypeTemplate,
14 },
15 };
16 use sqlx::PgPool;
17
18 use super::{ITEM_LABELS, ITEM_STEPS, build_step_nav, format_price_display};
19
20 pub(super) async fn render_step(
21 db: &PgPool,
22 session: &Session,
23 user: &crate::auth::SessionUser,
24 project: &db::DbProject,
25 item: &db::DbItem,
26 step: &str,
27 ) -> Result<Response> {
28 let nav = build_step_nav(ITEM_STEPS, ITEM_LABELS, step);
29 let project_slug = project.slug.to_string();
30 let item_id = item.id.to_string();
31 let csrf_token = get_csrf_token(session).await;
32
33 match step {
34 "type" => {
35 let type_cards = ProjectFeature::wizard_type_cards(&project.features);
36 // If only 1 behavior group, skip forward to basics
37 if type_cards.len() <= 1 {
38 let nav = build_step_nav(ITEM_STEPS, ITEM_LABELS, "basics");
39 return Ok(WizardItemBasicsTemplate {
40 nav,
41 project_slug,
42 item_id,
43 title: item.title.clone(),
44 description: item.description.clone().unwrap_or_default(),
45 cover_image_url: item.cover_image_url.clone(),
46 }
47 .into_response());
48 }
49 Ok(WizardItemTypeTemplate {
50 nav,
51 project_slug,
52 item_id,
53 item_type_cards: type_cards,
54 selected_type: item.item_type.to_string(),
55 }
56 .into_response())
57 }
58
59 "basics" => Ok(WizardItemBasicsTemplate {
60 nav,
61 project_slug,
62 item_id,
63 title: item.title.clone(),
64 description: item.description.clone().unwrap_or_default(),
65 cover_image_url: item.cover_image_url.clone(),
66 }
67 .into_response()),
68
69 "content" => {
70 let content_template = item.item_type.to_string();
71
72 // Load bundle data if this is a bundle item
73 let (bundleable_items, selected_bundle_ids, unlisted_ids) =
74 if item.item_type == ItemType::Bundle {
75 let bundleable =
76 db::bundles::get_bundleable_items(db, project.id, Some(item.id)).await?;
77 let current = db::bundles::get_bundle_items(db, item.id).await?;
78 let selected: Vec<String> = current.iter().map(|i| i.id.to_string()).collect();
79 let unlisted: Vec<String> = current
80 .iter()
81 .chain(bundleable.iter())
82 .filter(|i| !i.listed)
83 .map(|i| i.id.to_string())
84 .collect();
85 let bi: Vec<BundleableItem> = bundleable
86 .iter()
87 .map(|i| BundleableItem {
88 id: i.id.to_string(),
89 title: i.title.clone(),
90 item_type: i.item_type.label().to_string(),
91 })
92 .collect();
93 (bi, selected, unlisted)
94 } else {
95 (vec![], vec![], vec![])
96 };
97
98 Ok(WizardItemContentTemplate {
99 nav,
100 project_slug,
101 project_id: project.id.to_string(),
102 item_id,
103 item_type: content_template,
104 body: item.body.clone().unwrap_or_default(),
105 bundleable_items,
106 selected_bundle_ids,
107 unlisted_ids,
108 }
109 .into_response())
110 }
111
112 "sections" => {
113 let db_sections = db::item_sections::list_by_item(db, item.id).await?;
114 let sections: Vec<crate::types::ItemSection> = db_sections
115 .iter()
116 .map(crate::types::ItemSection::from)
117 .collect();
118 Ok(WizardItemSectionsTemplate {
119 nav,
120 project_slug,
121 item_id,
122 sections,
123 }
124 .into_response())
125 }
126
127 "pricing" => {
128 let pricing_model = if item.pwyw_enabled {
129 "pwyw"
130 } else if item.price_cents > 0 {
131 "fixed"
132 } else {
133 "free"
134 };
135
136 Ok(WizardItemPricingTemplate {
137 nav,
138 project_slug,
139 item_id,
140 pricing_model: pricing_model.to_string(),
141 price_dollars: format!("{}.{:02}", item.price_cents / 100, item.price_cents % 100),
142 pwyw_suggested_dollars: format!(
143 "{}.{:02}",
144 item.price_cents / 100,
145 item.price_cents % 100
146 ),
147 pwyw_min_dollars: {
148 let min = item.pwyw_min_cents.unwrap_or(0);
149 format!("{}.{:02}", min / 100, min % 100)
150 },
151 }
152 .into_response())
153 }
154
155 "preview" => {
156 let tags = db::tags::get_tags_for_item(db, item.id).await?;
157 let tag_names: Vec<String> = tags.iter().map(|t| t.tag_name.clone()).collect();
158
159 Ok(WizardItemPreviewTemplate {
160 csrf_token,
161 nav,
162 project_slug,
163 item_id,
164 title: item.title.clone(),
165 item_type: item.item_type.to_string(),
166 description: item.description.clone().unwrap_or_default(),
167 price_display: format_price_display(
168 item.price_cents,
169 item.pwyw_enabled,
170 item.pwyw_min_cents,
171 user.settlement_currency,
172 ),
173 tag_names,
174 has_content: item.body.is_some()
175 || item.audio_s3_key.is_some()
176 || item.video_s3_key.is_some()
177 || (item.item_type == ItemType::Bundle
178 && db::bundles::get_bundle_item_count(db, item.id).await? > 0),
179 is_public: item.is_public,
180 }
181 .into_response())
182 }
183
184 _ => Err(AppError::NotFound),
185 }
186 }
187