max / makenotwork
5 files changed,
+131 insertions,
-102 deletions
| @@ -9,14 +9,16 @@ use axum::{ | |||
| 9 | 9 | http::{HeaderMap, StatusCode, Uri}, | |
| 10 | 10 | response::{IntoResponse, Response}, | |
| 11 | 11 | }; | |
| 12 | + | use sqlx::PgPool; | |
| 12 | 13 | use tower_sessions::Session; | |
| 13 | 14 | ||
| 14 | 15 | use crate::{ | |
| 15 | 16 | auth::{MaybeUserVerified, SessionUser}, | |
| 17 | + | config::Config, | |
| 16 | 18 | db::{self, Slug}, | |
| 17 | 19 | error::{AppError, Result}, | |
| 18 | 20 | helpers::get_csrf_token, | |
| 19 | - | AppState, | |
| 21 | + | AppCaches, Integrations, | |
| 20 | 22 | }; | |
| 21 | 23 | ||
| 22 | 24 | use super::pages::public::content; | |
| @@ -50,8 +52,12 @@ fn is_mnw_domain(host: &str) -> bool { | |||
| 50 | 52 | /// | |
| 51 | 53 | /// Called from named route handlers (e.g. `landing::index` for `/`) where the | |
| 52 | 54 | /// fallback handler wouldn't fire because the route is already matched. | |
| 55 | + | #[allow(clippy::too_many_arguments)] | |
| 53 | 56 | pub async fn try_handle( | |
| 54 | - | state: &AppState, | |
| 57 | + | db: &PgPool, | |
| 58 | + | caches: &AppCaches, | |
| 59 | + | integrations: &Integrations, | |
| 60 | + | config: &Config, | |
| 55 | 61 | headers: &HeaderMap, | |
| 56 | 62 | path: &str, | |
| 57 | 63 | session: &Session, | |
| @@ -63,7 +69,7 @@ pub async fn try_handle( | |||
| 63 | 69 | return None; | |
| 64 | 70 | } | |
| 65 | 71 | ||
| 66 | - | let user_id = state.caches.domain_cache.get(&host).map(|e| *e.value())?; | |
| 72 | + | let user_id = caches.domain_cache.get(&host).map(|e| *e.value())?; | |
| 67 | 73 | ||
| 68 | 74 | let segments: Vec<&str> = path | |
| 69 | 75 | .trim_start_matches('/') | |
| @@ -72,10 +78,12 @@ pub async fn try_handle( | |||
| 72 | 78 | .collect(); | |
| 73 | 79 | ||
| 74 | 80 | let result = match segments.as_slice() { | |
| 75 | - | [] => render_user_profile(state, user_id, session, maybe_user).await, | |
| 76 | - | [project_slug] => render_project(state, user_id, project_slug, session, maybe_user).await, | |
| 81 | + | [] => render_user_profile(db, config, user_id, session, maybe_user).await, | |
| 82 | + | [project_slug] => { | |
| 83 | + | render_project(db, config, user_id, project_slug, session, maybe_user).await | |
| 84 | + | } | |
| 77 | 85 | [project_slug, item_slug] => { | |
| 78 | - | render_item(state, user_id, project_slug, item_slug, session, maybe_user).await | |
| 86 | + | render_item(db, integrations, config, user_id, project_slug, item_slug, session, maybe_user).await | |
| 79 | 87 | } | |
| 80 | 88 | _ => return Some(StatusCode::NOT_FOUND.into_response()), | |
| 81 | 89 | }; | |
| @@ -103,8 +111,12 @@ pub async fn try_handle( | |||
| 103 | 111 | /// | |
| 104 | 112 | /// MNW domains get a standard 404. | |
| 105 | 113 | #[tracing::instrument(skip_all, name = "custom_domain::fallback")] | |
| 114 | + | #[allow(clippy::too_many_arguments)] | |
| 106 | 115 | pub async fn custom_domain_fallback( | |
| 107 | - | State(state): State<AppState>, | |
| 116 | + | State(db): State<PgPool>, | |
| 117 | + | State(caches): State<AppCaches>, | |
| 118 | + | State(integrations): State<Integrations>, | |
| 119 | + | State(config): State<Config>, | |
| 108 | 120 | headers: HeaderMap, | |
| 109 | 121 | uri: Uri, | |
| 110 | 122 | session: Session, | |
| @@ -120,7 +132,7 @@ pub async fn custom_domain_fallback( | |||
| 120 | 132 | } | |
| 121 | 133 | ||
| 122 | 134 | // Look up custom domain | |
| 123 | - | let user_id = match state.caches.domain_cache.get(&host) { | |
| 135 | + | let user_id = match caches.domain_cache.get(&host) { | |
| 124 | 136 | Some(entry) => *entry.value(), | |
| 125 | 137 | None => return StatusCode::NOT_FOUND.into_response(), | |
| 126 | 138 | }; | |
| @@ -130,12 +142,12 @@ pub async fn custom_domain_fallback( | |||
| 130 | 142 | let segments: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect(); | |
| 131 | 143 | ||
| 132 | 144 | let result = match segments.as_slice() { | |
| 133 | - | [] => render_user_profile(&state, user_id, &session, &maybe_user).await, | |
| 145 | + | [] => render_user_profile(&db, &config, user_id, &session, &maybe_user).await, | |
| 134 | 146 | [project_slug] => { | |
| 135 | - | render_project(&state, user_id, project_slug, &session, &maybe_user).await | |
| 147 | + | render_project(&db, &config, user_id, project_slug, &session, &maybe_user).await | |
| 136 | 148 | } | |
| 137 | 149 | [project_slug, item_slug] => { | |
| 138 | - | render_item(&state, user_id, project_slug, item_slug, &session, &maybe_user).await | |
| 150 | + | render_item(&db, &integrations, &config, user_id, project_slug, item_slug, &session, &maybe_user).await | |
| 139 | 151 | } | |
| 140 | 152 | _ => return StatusCode::NOT_FOUND.into_response(), | |
| 141 | 153 | }; | |
| @@ -154,21 +166,23 @@ pub async fn custom_domain_fallback( | |||
| 154 | 166 | ||
| 155 | 167 | /// Render a user profile for a custom domain. | |
| 156 | 168 | async fn render_user_profile( | |
| 157 | - | state: &AppState, | |
| 169 | + | db: &PgPool, | |
| 170 | + | config: &Config, | |
| 158 | 171 | user_id: db::UserId, | |
| 159 | 172 | session: &Session, | |
| 160 | 173 | maybe_user: &Option<SessionUser>, | |
| 161 | 174 | ) -> Result<Response> { | |
| 162 | 175 | let csrf_token = get_csrf_token(session).await; | |
| 163 | - | let db_user = db::users::get_user_by_id(&state.db, user_id) | |
| 176 | + | let db_user = db::users::get_user_by_id(db, user_id) | |
| 164 | 177 | .await? | |
| 165 | 178 | .ok_or(AppError::NotFound)?; | |
| 166 | - | content::render_user_profile(state, &db_user, csrf_token, maybe_user.clone()).await | |
| 179 | + | content::render_user_profile(db, config, &db_user, csrf_token, maybe_user.clone()).await | |
| 167 | 180 | } | |
| 168 | 181 | ||
| 169 | 182 | /// Render a project page for a custom domain (scoped to user_id + slug). | |
| 170 | 183 | async fn render_project( | |
| 171 | - | state: &AppState, | |
| 184 | + | db: &PgPool, | |
| 185 | + | config: &Config, | |
| 172 | 186 | user_id: db::UserId, | |
| 173 | 187 | project_slug: &str, | |
| 174 | 188 | session: &Session, | |
| @@ -177,15 +191,18 @@ async fn render_project( | |||
| 177 | 191 | let csrf_token = get_csrf_token(session).await; | |
| 178 | 192 | let slug = Slug::new(project_slug).map_err(|_| AppError::NotFound)?; | |
| 179 | 193 | let db_project = | |
| 180 | - | db::projects::get_public_project_by_user_and_slug(&state.db, user_id, &slug) | |
| 194 | + | db::projects::get_public_project_by_user_and_slug(db, user_id, &slug) | |
| 181 | 195 | .await? | |
| 182 | 196 | .ok_or(AppError::NotFound)?; | |
| 183 | - | content::render_project_page(state, &db_project, csrf_token, maybe_user.clone()).await | |
| 197 | + | content::render_project_page(db, config, &db_project, csrf_token, maybe_user.clone()).await | |
| 184 | 198 | } | |
| 185 | 199 | ||
| 186 | 200 | /// Render an item page for a custom domain (scoped to user_id + project slug + item slug). | |
| 201 | + | #[allow(clippy::too_many_arguments)] | |
| 187 | 202 | async fn render_item( | |
| 188 | - | state: &AppState, | |
| 203 | + | db: &PgPool, | |
| 204 | + | integrations: &Integrations, | |
| 205 | + | config: &Config, | |
| 189 | 206 | user_id: db::UserId, | |
| 190 | 207 | project_slug: &str, | |
| 191 | 208 | item_slug: &str, | |
| @@ -195,16 +212,16 @@ async fn render_item( | |||
| 195 | 212 | let csrf_token = get_csrf_token(session).await; | |
| 196 | 213 | let slug = Slug::new(project_slug).map_err(|_| AppError::NotFound)?; | |
| 197 | 214 | let db_project = | |
| 198 | - | db::projects::get_public_project_by_user_and_slug(&state.db, user_id, &slug) | |
| 215 | + | db::projects::get_public_project_by_user_and_slug(db, user_id, &slug) | |
| 199 | 216 | .await? | |
| 200 | 217 | .ok_or(AppError::NotFound)?; | |
| 201 | - | let db_item = db::items::get_item_by_project_and_slug(&state.db, db_project.id, item_slug) | |
| 218 | + | let db_item = db::items::get_item_by_project_and_slug(db, db_project.id, item_slug) | |
| 202 | 219 | .await? | |
| 203 | 220 | .ok_or(AppError::NotFound)?; | |
| 204 | - | let db_user = db::users::get_user_by_id(&state.db, db_project.user_id) | |
| 221 | + | let db_user = db::users::get_user_by_id(db, db_project.user_id) | |
| 205 | 222 | .await? | |
| 206 | 223 | .ok_or(AppError::NotFound)?; | |
| 207 | - | content::render_item_page(state, &db_item, &db_project, &db_user, csrf_token, maybe_user.clone()).await | |
| 224 | + | content::render_item_page(db, integrations, config, &db_item, &db_project, &db_user, csrf_token, maybe_user.clone()).await | |
| 208 | 225 | } | |
| 209 | 226 | ||
| 210 | 227 | #[cfg(test)] |
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | //! Public item detail page handler. | |
| 2 | 2 | ||
| 3 | 3 | use axum::{ | |
| 4 | - | extract::{FromRef, Path, State}, | |
| 4 | + | extract::{Path, State}, | |
| 5 | 5 | response::{IntoResponse, Response}, | |
| 6 | 6 | }; | |
| 7 | 7 | use sqlx::PgPool; | |
| @@ -9,32 +9,35 @@ use tower_sessions::Session; | |||
| 9 | 9 | ||
| 10 | 10 | use crate::{ | |
| 11 | 11 | auth::{MaybeUserVerified, SessionUser}, | |
| 12 | + | config::Config, | |
| 12 | 13 | db::{self, ContentData, ItemId, ItemType}, | |
| 13 | 14 | error::{AppError, Result}, | |
| 14 | 15 | helpers::{fetch_discussion_info, get_csrf_token, get_initials}, | |
| 15 | 16 | pricing, | |
| 16 | 17 | templates::*, | |
| 17 | 18 | types::*, | |
| 18 | - | AppState, AppStorage, | |
| 19 | + | AppStorage, Integrations, | |
| 19 | 20 | }; | |
| 20 | 21 | ||
| 21 | 22 | /// Render a public item detail page (text reader, audio player, or download). | |
| 22 | 23 | #[tracing::instrument(skip_all, name = "content::item_page")] | |
| 23 | 24 | pub(in crate::routes::pages::public) async fn item_page( | |
| 24 | - | State(state): State<AppState>, | |
| 25 | + | State(db): State<PgPool>, | |
| 26 | + | State(integrations): State<Integrations>, | |
| 27 | + | State(config): State<Config>, | |
| 25 | 28 | session: Session, | |
| 26 | 29 | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| 27 | 30 | Path(item_id): Path<String>, | |
| 28 | 31 | ) -> Result<Response> { | |
| 29 | 32 | let csrf_token = get_csrf_token(&session).await; | |
| 30 | 33 | let id: ItemId = item_id.parse().map_err(|_| AppError::NotFound)?; | |
| 31 | - | let db_item = db::items::get_item_by_id(&state.db, id) | |
| 34 | + | let db_item = db::items::get_item_by_id(&db, id) | |
| 32 | 35 | .await? | |
| 33 | 36 | .ok_or(AppError::NotFound)?; | |
| 34 | - | let db_project = db::projects::get_project_by_id(&state.db, db_item.project_id) | |
| 37 | + | let db_project = db::projects::get_project_by_id(&db, db_item.project_id) | |
| 35 | 38 | .await? | |
| 36 | 39 | .ok_or(AppError::NotFound)?; | |
| 37 | - | let db_user = db::users::get_user_by_id(&state.db, db_project.user_id) | |
| 40 | + | let db_user = db::users::get_user_by_id(&db, db_project.user_id) | |
| 38 | 41 | .await? | |
| 39 | 42 | .ok_or(AppError::NotFound)?; | |
| 40 | 43 | if db_user.is_sandbox { | |
| @@ -42,12 +45,15 @@ pub(in crate::routes::pages::public) async fn item_page( | |||
| 42 | 45 | } | |
| 43 | 46 | // View tracking moved to /l/{id} — consumption is the meaningful signal, | |
| 44 | 47 | // not store-page traffic. | |
| 45 | - | render_item_page(&state, &db_item, &db_project, &db_user, csrf_token, maybe_user).await | |
| 48 | + | render_item_page(&db, &integrations, &config, &db_item, &db_project, &db_user, csrf_token, maybe_user).await | |
| 46 | 49 | } | |
| 47 | 50 | ||
| 48 | 51 | /// Shared item page renderer, used by both named routes and custom domain fallback. | |
| 52 | + | #[allow(clippy::too_many_arguments)] | |
| 49 | 53 | pub(crate) async fn render_item_page( | |
| 50 | - | state: &AppState, | |
| 54 | + | db: &PgPool, | |
| 55 | + | integrations: &Integrations, | |
| 56 | + | config: &Config, | |
| 51 | 57 | db_item: &db::DbItem, | |
| 52 | 58 | db_project: &db::DbProject, | |
| 53 | 59 | db_user: &db::DbUser, | |
| @@ -66,11 +72,7 @@ pub(crate) async fn render_item_page( | |||
| 66 | 72 | return Err(AppError::NotFound); | |
| 67 | 73 | } | |
| 68 | 74 | ||
| 69 | - | let cdn_base = state.config.cdn_base_url.as_deref().unwrap_or("https://cdn.makenot.work"); | |
| 70 | - | // render_item_page keeps `&AppState` (it is shared with the custom-domain | |
| 71 | - | // fallback in `custom_domain.rs`), so project the narrowed integration slice | |
| 72 | - | // here for the now-slimmed `fetch_discussion_info`. | |
| 73 | - | let integrations = crate::Integrations::from_ref(state); | |
| 75 | + | let cdn_base = config.cdn_base_url.as_deref().unwrap_or("https://cdn.makenot.work"); | |
| 74 | 76 | // Store page never renders the full article body — that lives on /l/{id}. | |
| 75 | 77 | // Compute a short plain-text excerpt from the raw markdown for the deck. | |
| 76 | 78 | let (excerpt, reading_time) = match db_item.content() { | |
| @@ -87,12 +89,12 @@ pub(crate) async fn render_item_page( | |||
| 87 | 89 | ||
| 88 | 90 | let item_pricing = pricing::for_item(db_item); | |
| 89 | 91 | let in_library = if let Some(ref user) = maybe_user { | |
| 90 | - | db::transactions::has_purchased_item(&state.db, user.id, db_item.id).await? | |
| 92 | + | db::transactions::has_purchased_item(db, user.id, db_item.id).await? | |
| 91 | 93 | } else { | |
| 92 | 94 | false | |
| 93 | 95 | }; | |
| 94 | 96 | let item_sub = if let Some(ref user) = maybe_user { | |
| 95 | - | db::subscriptions::SubscriptionGate::check(&state.db, user.id, db::subscriptions::SubscriptionScope::Item(db_item.id)).await? | |
| 97 | + | db::subscriptions::SubscriptionGate::check(db, user.id, db::subscriptions::SubscriptionScope::Item(db_item.id)).await? | |
| 96 | 98 | } else { | |
| 97 | 99 | None | |
| 98 | 100 | }; | |
| @@ -107,29 +109,29 @@ pub(crate) async fn render_item_page( | |||
| 107 | 109 | // Bundle access: user may have purchased a bundle containing this item | |
| 108 | 110 | if !has_access | |
| 109 | 111 | && let Some(ref user) = maybe_user | |
| 110 | - | && db::bundles::has_access_via_bundle(&state.db, user.id, db_item.id).await? | |
| 112 | + | && db::bundles::has_access_via_bundle(db, user.id, db_item.id).await? | |
| 111 | 113 | { | |
| 112 | 114 | has_access = true; | |
| 113 | 115 | } | |
| 114 | 116 | ||
| 115 | 117 | // For unlisted items, load the bundles that contain them (for "Available in" display) | |
| 116 | 118 | let containing_bundle_ids = if !db_item.listed { | |
| 117 | - | db::bundles::get_bundles_containing_item(&state.db, db_item.id).await? | |
| 119 | + | db::bundles::get_bundles_containing_item(db, db_item.id).await? | |
| 118 | 120 | } else { | |
| 119 | 121 | vec![] | |
| 120 | 122 | }; | |
| 121 | 123 | // Single batched query instead of one get_item_by_id per bundle id (N+1). | |
| 122 | 124 | let containing_bundles: Vec<db::DbItem> = | |
| 123 | - | db::items::get_public_items_by_ids(&state.db, &containing_bundle_ids).await?; | |
| 125 | + | db::items::get_public_items_by_ids(db, &containing_bundle_ids).await?; | |
| 124 | 126 | ||
| 125 | 127 | // For bundle-type items, load the child items | |
| 126 | 128 | let bundle_child_items = if db_item.item_type == ItemType::Bundle { | |
| 127 | - | db::bundles::get_bundle_items(&state.db, db_item.id).await? | |
| 129 | + | db::bundles::get_bundle_items(db, db_item.id).await? | |
| 128 | 130 | } else { | |
| 129 | 131 | vec![] | |
| 130 | 132 | }; | |
| 131 | 133 | ||
| 132 | - | let item_tags = db::tags::get_tags_for_item(&state.db, db_item.id).await?; | |
| 134 | + | let item_tags = db::tags::get_tags_for_item(db, db_item.id).await?; | |
| 133 | 135 | let item = Item::from_db_detail( | |
| 134 | 136 | db_item, | |
| 135 | 137 | &item_tags, | |
| @@ -144,7 +146,7 @@ pub(crate) async fn render_item_page( | |||
| 144 | 146 | get_initials(db_user.display_name.as_deref().unwrap_or(&db_user.username)); | |
| 145 | 147 | let project_slug_str = db_project.slug.to_string(); | |
| 146 | 148 | let (discussion_url, discussion_count) = | |
| 147 | - | fetch_discussion_info(&integrations, &state.config, db_item.mt_thread_id, &project_slug_str, "items").await; | |
| 149 | + | fetch_discussion_info(integrations, config, db_item.mt_thread_id, &project_slug_str, "items").await; | |
| 148 | 150 | return Ok(TextReaderTemplate { | |
| 149 | 151 | csrf_token: csrf_token.clone(), | |
| 150 | 152 | session_user: maybe_user, | |
| @@ -159,7 +161,7 @@ pub(crate) async fn render_item_page( | |||
| 159 | 161 | has_access, | |
| 160 | 162 | reading_time, | |
| 161 | 163 | excerpt, | |
| 162 | - | host_url: state.config.host_url.clone(), | |
| 164 | + | host_url: config.host_url.clone(), | |
| 163 | 165 | discussion_url, | |
| 164 | 166 | discussion_count, | |
| 165 | 167 | } | |
| @@ -171,7 +173,7 @@ pub(crate) async fn render_item_page( | |||
| 171 | 173 | get_initials(db_user.display_name.as_deref().unwrap_or(&db_user.username)); | |
| 172 | 174 | let project_slug_str = db_project.slug.to_string(); | |
| 173 | 175 | let (discussion_url, discussion_count) = | |
| 174 | - | fetch_discussion_info(&integrations, &state.config, db_item.mt_thread_id, &project_slug_str, "items").await; | |
| 176 | + | fetch_discussion_info(integrations, config, db_item.mt_thread_id, &project_slug_str, "items").await; | |
| 175 | 177 | return Ok(AudioPlayerTemplate { | |
| 176 | 178 | csrf_token: csrf_token.clone(), | |
| 177 | 179 | session_user: maybe_user, | |
| @@ -184,7 +186,7 @@ pub(crate) async fn render_item_page( | |||
| 184 | 186 | is_free, | |
| 185 | 187 | in_library, | |
| 186 | 188 | has_access, | |
| 187 | - | host_url: state.config.host_url.clone(), | |
| 189 | + | host_url: config.host_url.clone(), | |
| 188 | 190 | discussion_url, | |
| 189 | 191 | discussion_count, | |
| 190 | 192 | } | |
| @@ -196,7 +198,7 @@ pub(crate) async fn render_item_page( | |||
| 196 | 198 | get_initials(db_user.display_name.as_deref().unwrap_or(&db_user.username)); | |
| 197 | 199 | let project_slug_str = db_project.slug.to_string(); | |
| 198 | 200 | let (discussion_url, discussion_count) = | |
| 199 | - | fetch_discussion_info(&integrations, &state.config, db_item.mt_thread_id, &project_slug_str, "items").await; | |
| 201 | + | fetch_discussion_info(integrations, config, db_item.mt_thread_id, &project_slug_str, "items").await; | |
| 200 | 202 | return Ok(VideoPlayerTemplate { | |
| 201 | 203 | csrf_token: csrf_token.clone(), | |
| 202 | 204 | session_user: maybe_user, | |
| @@ -209,7 +211,7 @@ pub(crate) async fn render_item_page( | |||
| 209 | 211 | is_free, | |
| 210 | 212 | in_library, | |
| 211 | 213 | has_access, | |
| 212 | - | host_url: state.config.host_url.clone(), | |
| 214 | + | host_url: config.host_url.clone(), | |
| 213 | 215 | discussion_url, | |
| 214 | 216 | discussion_count, | |
| 215 | 217 | } | |
| @@ -218,7 +220,7 @@ pub(crate) async fn render_item_page( | |||
| 218 | 220 | ||
| 219 | 221 | let project_slug_str = db_project.slug.to_string(); | |
| 220 | 222 | let (discussion_url, discussion_count) = | |
| 221 | - | fetch_discussion_info(&integrations, &state.config, db_item.mt_thread_id, &project_slug_str, "items").await; | |
| 223 | + | fetch_discussion_info(integrations, config, db_item.mt_thread_id, &project_slug_str, "items").await; | |
| 222 | 224 | ||
| 223 | 225 | // Convert bundle child items to view models | |
| 224 | 226 | let bundle_item_views: Vec<Item> = bundle_child_items | |
| @@ -238,14 +240,14 @@ pub(crate) async fn render_item_page( | |||
| 238 | 240 | }) | |
| 239 | 241 | .collect(); | |
| 240 | 242 | ||
| 241 | - | let db_sections = db::item_sections::list_by_item(&state.db, db_item.id).await?; | |
| 243 | + | let db_sections = db::item_sections::list_by_item(db, db_item.id).await?; | |
| 242 | 244 | let sections: Vec<ItemSection> = db_sections.iter().map(|s| ItemSection::from_db(s, db_project.user_id, cdn_base)).collect(); | |
| 243 | 245 | ||
| 244 | 246 | // Wishlist / cart / collection-count for the viewer, collapsed into one query | |
| 245 | 247 | // (was three sequential round-trips). Display-only; a failure falls back to | |
| 246 | 248 | // the empty default, matching the prior per-call error tolerance. | |
| 247 | 249 | let viewer_flags = if let Some(ref user) = maybe_user { | |
| 248 | - | db::items::get_viewer_item_flags(&state.db, user.id, db_item.id) | |
| 250 | + | db::items::get_viewer_item_flags(db, user.id, db_item.id) | |
| 249 | 251 | .await | |
| 250 | 252 | .unwrap_or_default() | |
| 251 | 253 | } else { | |
| @@ -259,7 +261,7 @@ pub(crate) async fn render_item_page( | |||
| 259 | 261 | // creator-optional; fall back to a title-based description rather than an | |
| 260 | 262 | // empty string (the carousel relies on alt for screen-reader parity, and | |
| 261 | 263 | // CarouselFrame::new debug-asserts non-empty, so build the struct directly). | |
| 262 | - | let gallery = db::gallery_images::list_for_item(&state.db, db_item.id) | |
| 264 | + | let gallery = db::gallery_images::list_for_item(db, db_item.id) | |
| 263 | 265 | .await | |
| 264 | 266 | .unwrap_or_default() | |
| 265 | 267 | .into_iter() | |
| @@ -281,7 +283,7 @@ pub(crate) async fn render_item_page( | |||
| 281 | 283 | creator_username: db_user.username.to_string(), | |
| 282 | 284 | project_title: db_project.title.clone(), | |
| 283 | 285 | project_slug: project_slug_str, | |
| 284 | - | host_url: state.config.host_url.clone(), | |
| 286 | + | host_url: config.host_url.clone(), | |
| 285 | 287 | project_cover_image_url: db_project.cover_image_url.clone(), | |
| 286 | 288 | discussion_url, | |
| 287 | 289 | discussion_count, |
| @@ -26,7 +26,6 @@ use crate::{ | |||
| 26 | 26 | helpers::get_csrf_token, | |
| 27 | 27 | templates::*, | |
| 28 | 28 | types::*, | |
| 29 | - | AppState, | |
| 30 | 29 | }; | |
| 31 | 30 | ||
| 32 | 31 | /// Fire-and-forget page view recording. Never blocks the response. | |
| @@ -67,7 +66,9 @@ pub struct PurchaseQuery { | |||
| 67 | 66 | /// Render a public user profile page with projects and custom links. | |
| 68 | 67 | #[tracing::instrument(skip_all, name = "content::user_page")] | |
| 69 | 68 | pub(super) async fn user_page( | |
| 70 | - | State(state): State<AppState>, | |
| 69 | + | State(db): State<PgPool>, | |
| 70 | + | State(config): State<Config>, | |
| 71 | + | State(page_view_tx): State<crate::db::page_views::PageViewTx>, | |
| 71 | 72 | session: Session, | |
| 72 | 73 | headers: axum::http::HeaderMap, | |
| 73 | 74 | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| @@ -75,49 +76,50 @@ pub(super) async fn user_page( | |||
| 75 | 76 | ) -> Result<Response> { | |
| 76 | 77 | let csrf_token = get_csrf_token(&session).await; | |
| 77 | 78 | let username = Username::new(&username).map_err(|_| AppError::NotFound)?; | |
| 78 | - | let db_user = db::users::get_user_by_username(&state.db, &username) | |
| 79 | + | let db_user = db::users::get_user_by_username(&db, &username) | |
| 79 | 80 | .await? | |
| 80 | 81 | .ok_or(AppError::NotFound)?; | |
| 81 | 82 | // Sandbox accounts are not publicly visible | |
| 82 | 83 | if db_user.is_sandbox { | |
| 83 | 84 | return Err(AppError::NotFound); | |
| 84 | 85 | } | |
| 85 | - | let response = render_user_profile(&state, &db_user, csrf_token, maybe_user).await?; | |
| 86 | + | let response = render_user_profile(&db, &config, &db_user, csrf_token, maybe_user).await?; | |
| 86 | 87 | let ua = headers.get(axum::http::header::USER_AGENT) | |
| 87 | 88 | .and_then(|v| v.to_str().ok()) | |
| 88 | 89 | .unwrap_or(""); | |
| 89 | 90 | if !is_bot(ua) { | |
| 90 | - | track_view(&state.page_view_tx, "user", *db_user.id); | |
| 91 | + | track_view(&page_view_tx, "user", *db_user.id); | |
| 91 | 92 | } | |
| 92 | 93 | Ok(response) | |
| 93 | 94 | } | |
| 94 | 95 | ||
| 95 | 96 | /// Shared user profile renderer, used by both named routes and custom domain fallback. | |
| 96 | 97 | pub(crate) async fn render_user_profile( | |
| 97 | - | state: &AppState, | |
| 98 | + | db: &PgPool, | |
| 99 | + | config: &Config, | |
| 98 | 100 | db_user: &db::DbUser, | |
| 99 | 101 | csrf_token: Option<String>, | |
| 100 | 102 | maybe_user: Option<SessionUser>, | |
| 101 | 103 | ) -> Result<Response> { | |
| 102 | 104 | let db_projects = | |
| 103 | - | db::projects::get_public_projects_with_item_counts(&state.db, db_user.id).await?; | |
| 104 | - | let db_links = db::custom_links::get_custom_links_by_user(&state.db, db_user.id).await?; | |
| 105 | + | db::projects::get_public_projects_with_item_counts(db, db_user.id).await?; | |
| 106 | + | let db_links = db::custom_links::get_custom_links_by_user(db, db_user.id).await?; | |
| 105 | 107 | ||
| 106 | 108 | let user = User::from(db_user); | |
| 107 | 109 | let projects: Vec<Project> = db_projects.iter().map(Project::from).collect(); | |
| 108 | 110 | let custom_links: Vec<CustomLink> = db_links.iter().map(CustomLink::from).collect(); | |
| 109 | 111 | ||
| 110 | 112 | let db_collections = | |
| 111 | - | db::collections::get_public_collections_by_user(&state.db, db_user.id).await?; | |
| 113 | + | db::collections::get_public_collections_by_user(db, db_user.id).await?; | |
| 112 | 114 | let public_collections: Vec<Collection> = | |
| 113 | 115 | db_collections.iter().map(Collection::from).collect(); | |
| 114 | 116 | ||
| 115 | 117 | let follower_count = | |
| 116 | - | db::follows::get_follower_count(&state.db, FollowTargetType::User, db_user.id.into()) | |
| 118 | + | db::follows::get_follower_count(db, FollowTargetType::User, db_user.id.into()) | |
| 117 | 119 | .await?; | |
| 118 | 120 | let is_following = if let Some(ref viewer) = maybe_user { | |
| 119 | 121 | db::follows::is_following( | |
| 120 | - | &state.db, | |
| 122 | + | db, | |
| 121 | 123 | viewer.id, | |
| 122 | 124 | FollowTargetType::User, | |
| 123 | 125 | db_user.id.into(), | |
| @@ -144,7 +146,7 @@ pub(crate) async fn render_user_profile( | |||
| 144 | 146 | is_own_profile, | |
| 145 | 147 | is_following, | |
| 146 | 148 | follower_count, | |
| 147 | - | host_url: state.config.host_url.clone(), | |
| 149 | + | host_url: config.host_url.clone(), | |
| 148 | 150 | theme_css: crate::theming::theme_css(db_user.theme_id.as_deref()), | |
| 149 | 151 | } | |
| 150 | 152 | .into_response()) |
| @@ -4,23 +4,26 @@ use axum::{ | |||
| 4 | 4 | extract::{Path, State}, | |
| 5 | 5 | response::{IntoResponse, Response}, | |
| 6 | 6 | }; | |
| 7 | + | use sqlx::PgPool; | |
| 7 | 8 | use tower_sessions::Session; | |
| 8 | 9 | ||
| 9 | 10 | use crate::{ | |
| 10 | 11 | auth::{MaybeUserVerified, SessionUser}, | |
| 12 | + | config::Config, | |
| 11 | 13 | db::{self, FollowTargetType, ItemId, ItemType, Slug}, | |
| 12 | 14 | error::{AppError, Result}, | |
| 13 | 15 | helpers::get_csrf_token, | |
| 14 | 16 | pricing, | |
| 15 | 17 | templates::*, | |
| 16 | 18 | types::*, | |
| 17 | - | AppState, | |
| 18 | 19 | }; | |
| 19 | 20 | ||
| 20 | 21 | /// Render a public project page with its published items. | |
| 21 | 22 | #[tracing::instrument(skip_all, name = "content::project_page", fields(%slug))] | |
| 22 | 23 | pub(in crate::routes::pages::public) async fn project_page( | |
| 23 | - | State(state): State<AppState>, | |
| 24 | + | State(db): State<PgPool>, | |
| 25 | + | State(config): State<Config>, | |
| 26 | + | State(page_view_tx): State<crate::db::page_views::PageViewTx>, | |
| 24 | 27 | session: Session, | |
| 25 | 28 | headers: axum::http::HeaderMap, | |
| 26 | 29 | MaybeUserVerified(maybe_user): MaybeUserVerified, | |
| @@ -28,15 +31,15 @@ pub(in crate::routes::pages::public) async fn project_page( | |||
| 28 | 31 | ) -> Result<Response> { | |
| 29 | 32 | let csrf_token = get_csrf_token(&session).await; | |
| 30 | 33 | let slug = Slug::new(&slug).map_err(|_| AppError::NotFound)?; | |
| 31 | - | let db_project = db::projects::get_public_project_by_slug(&state.db, &slug) | |
| 34 | + | let db_project = db::projects::get_public_project_by_slug(&db, &slug) | |
| 32 | 35 | .await? | |
| 33 | 36 | .ok_or(AppError::NotFound)?; | |
| 34 | - | let response = render_project_page(&state, &db_project, csrf_token, maybe_user).await?; | |
| 37 | + | let response = render_project_page(&db, &config, &db_project, csrf_token, maybe_user).await?; | |
| 35 | 38 | let ua = headers.get(axum::http::header::USER_AGENT) | |
| 36 | 39 | .and_then(|v| v.to_str().ok()) | |
| 37 | 40 | .unwrap_or(""); | |
| 38 | 41 | if !super::is_bot(ua) { | |
| 39 | - | super::track_view(&state.page_view_tx, "project", *db_project.id); | |
| 42 | + | super::track_view(&page_view_tx, "project", *db_project.id); | |
| 40 | 43 | } | |
| 41 | 44 | Ok(response) | |
| 42 | 45 | } | |
| @@ -48,12 +51,13 @@ pub(in crate::routes::pages::public) async fn project_page( | |||
| 48 | 51 | fields(project_id = %db_project.id, project_slug = %db_project.slug, viewer_id = ?maybe_user.as_ref().map(|u| u.id)) | |
| 49 | 52 | )] | |
| 50 | 53 | pub(crate) async fn render_project_page( | |
| 51 | - | state: &AppState, | |
| 54 | + | db: &PgPool, | |
| 55 | + | config: &Config, | |
| 52 | 56 | db_project: &db::DbProject, | |
| 53 | 57 | csrf_token: Option<String>, | |
| 54 | 58 | maybe_user: Option<SessionUser>, | |
| 55 | 59 | ) -> Result<Response> { | |
| 56 | - | let db_user = db::users::get_user_by_id(&state.db, db_project.user_id) | |
| 60 | + | let db_user = db::users::get_user_by_id(db, db_project.user_id) | |
| 57 | 61 | .await? | |
| 58 | 62 | .ok_or(AppError::NotFound)?; | |
| 59 | 63 | ||
| @@ -61,7 +65,7 @@ pub(crate) async fn render_project_page( | |||
| 61 | 65 | let project_pricing = pricing::for_project(db_project); | |
| 62 | 66 | if !project_pricing.is_free() { | |
| 63 | 67 | let project_ctx = pricing::build_project_access_context( | |
| 64 | - | &state.db, | |
| 68 | + | db, | |
| 65 | 69 | maybe_user.as_ref().map(|u| u.id), | |
| 66 | 70 | db_project.id, | |
| 67 | 71 | db_project.user_id, | |
| @@ -80,7 +84,7 @@ pub(crate) async fn render_project_page( | |||
| 80 | 84 | "project paywall gate: showing paywall" | |
| 81 | 85 | ); | |
| 82 | 86 | let db_tiers = | |
| 83 | - | db::subscriptions::get_active_tiers_by_project(&state.db, db_project.id).await?; | |
| 87 | + | db::subscriptions::get_active_tiers_by_project(db, db_project.id).await?; | |
| 84 | 88 | let subscription_tiers: Vec<SubscriptionTier> = | |
| 85 | 89 | db_tiers.iter().map(SubscriptionTier::from).collect(); | |
| 86 | 90 | let project = Project::from_db(db_project, 0); | |
| @@ -92,13 +96,13 @@ pub(crate) async fn render_project_page( | |||
| 92 | 96 | price_display: project_pricing.price_display(), | |
| 93 | 97 | checkout_type: project_pricing.checkout_type(), | |
| 94 | 98 | subscription_tiers, | |
| 95 | - | host_url: state.config.host_url.clone(), | |
| 99 | + | host_url: config.host_url.clone(), | |
| 96 | 100 | } | |
| 97 | 101 | .into_response()); | |
| 98 | 102 | } | |
| 99 | 103 | } | |
| 100 | 104 | ||
| 101 | - | let db_items = db::items::get_public_items_by_project(&state.db, db_project.id).await?; | |
| 105 | + | let db_items = db::items::get_public_items_by_project(db, db_project.id).await?; | |
| 102 | 106 | ||
| 103 | 107 | let is_creator = maybe_user | |
| 104 | 108 | .as_ref() | |
| @@ -111,7 +115,7 @@ pub(crate) async fn render_project_page( | |||
| 111 | 115 | let purchased_item_ids: std::collections::HashSet<ItemId> = if let Some(ref user) = maybe_user | |
| 112 | 116 | { | |
| 113 | 117 | let item_ids: Vec<ItemId> = db_items.iter().map(|i| i.id).collect(); | |
| 114 | - | db::transactions::purchased_subset(&state.db, user.id, &item_ids).await? | |
| 118 | + | db::transactions::purchased_subset(db, user.id, &item_ids).await? | |
| 115 | 119 | } else { | |
| 116 | 120 | std::collections::HashSet::new() | |
| 117 | 121 | }; | |
| @@ -119,13 +123,13 @@ pub(crate) async fn render_project_page( | |||
| 119 | 123 | // Per-item subscription proofs for this user, so each item's AccessContext | |
| 120 | 124 | // gets its own gate witness rather than a bare membership bool. | |
| 121 | 125 | let subscribed_gates = if let Some(ref user) = maybe_user { | |
| 122 | - | db::subscriptions::SubscriptionGate::subscribed_item_gates(&state.db, user.id).await? | |
| 126 | + | db::subscriptions::SubscriptionGate::subscribed_item_gates(db, user.id).await? | |
| 123 | 127 | } else { | |
| 124 | 128 | std::collections::HashMap::new() | |
| 125 | 129 | }; | |
| 126 | 130 | ||
| 127 | 131 | let has_subscription = if let Some(ref user) = maybe_user { | |
| 128 | - | db::subscriptions::has_access(&state.db, user.id, db::subscriptions::SubscriptionScope::Project(db_project.id)) | |
| 132 | + | db::subscriptions::has_access(db, user.id, db::subscriptions::SubscriptionScope::Project(db_project.id)) | |
| 129 | 133 | .await? | |
| 130 | 134 | } else { | |
| 131 | 135 | false | |
| @@ -134,7 +138,7 @@ pub(crate) async fn render_project_page( | |||
| 134 | 138 | let project = Project::from_db(db_project, db_items.len() as u32); | |
| 135 | 139 | ||
| 136 | 140 | let item_ids: Vec<ItemId> = db_items.iter().map(|i| i.id).collect(); | |
| 137 | - | let tags_map = db::tags::get_tags_for_items(&state.db, &item_ids).await?; | |
| 141 | + | let tags_map = db::tags::get_tags_for_items(db, &item_ids).await?; | |
| 138 | 142 | ||
| 139 | 143 | // Batch child-counts for all bundles on the page in one query (was one | |
| 140 | 144 | // COUNT per bundle inside the loop below). | |
| @@ -146,7 +150,7 @@ pub(crate) async fn render_project_page( | |||
| 146 | 150 | let bundle_counts = if bundle_ids.is_empty() { | |
| 147 | 151 | std::collections::HashMap::new() | |
| 148 | 152 | } else { | |
| 149 | - | db::bundles::get_bundle_item_counts(&state.db, &bundle_ids).await? | |
| 153 | + | db::bundles::get_bundle_item_counts(db, &bundle_ids).await? | |
| 150 | 154 | }; | |
| 151 | 155 | ||
| 152 | 156 | let mut items: Vec<Item> = Vec::with_capacity(db_items.len()); | |
| @@ -168,14 +172,14 @@ pub(crate) async fn render_project_page( | |||
| 168 | 172 | } | |
| 169 | 173 | ||
| 170 | 174 | let follower_count = db::follows::get_follower_count( | |
| 171 | - | &state.db, | |
| 175 | + | db, | |
| 172 | 176 | FollowTargetType::Project, | |
| 173 | 177 | db_project.id.into(), | |
| 174 | 178 | ) | |
| 175 | 179 | .await?; | |
| 176 | 180 | let is_following = if let Some(ref viewer) = maybe_user { | |
| 177 | 181 | db::follows::is_following( | |
| 178 | - | &state.db, | |
| 182 | + | db, | |
| 179 | 183 | viewer.id, | |
| 180 | 184 | FollowTargetType::Project, | |
| 181 | 185 | db_project.id.into(), | |
| @@ -186,12 +190,12 @@ pub(crate) async fn render_project_page( | |||
| 186 | 190 | }; | |
| 187 | 191 | ||
| 188 | 192 | let db_tiers = | |
| 189 | - | db::subscriptions::get_active_tiers_by_project(&state.db, db_project.id).await?; | |
| 193 | + | db::subscriptions::get_active_tiers_by_project(db, db_project.id).await?; | |
| 190 | 194 | let subscription_tiers: Vec<SubscriptionTier> = | |
| 191 | 195 | db_tiers.iter().map(SubscriptionTier::from).collect(); | |
| 192 | 196 | ||
| 193 | - | let git_repos = if state.config.build.git_repos_path.is_some() { | |
| 194 | - | let linked = db::git_repos::get_repos_by_project(&state.db, db_project.id) | |
| 197 | + | let git_repos = if config.build.git_repos_path.is_some() { | |
| 198 | + | let linked = db::git_repos::get_repos_by_project(db, db_project.id) | |
| 195 | 199 | .await | |
| 196 | 200 | .unwrap_or_else(|e| { | |
| 197 | 201 | tracing::warn!(project_id = %db_project.id, error = %e, "linked git repos lookup failed; omitting repo links"); | |
| @@ -208,11 +212,10 @@ pub(crate) async fn render_project_page( | |||
| 208 | 212 | Vec::new() | |
| 209 | 213 | }; | |
| 210 | 214 | ||
| 211 | - | let has_blog_posts = db::blog_posts::has_published_posts(&state.db, db_project.id).await?; | |
| 215 | + | let has_blog_posts = db::blog_posts::has_published_posts(db, db_project.id).await?; | |
| 212 | 216 | ||
| 213 | 217 | let community_url = if db_project.mt_community_id.is_some() { | |
| 214 | - | state | |
| 215 | - | .config | |
| 218 | + | config | |
| 216 | 219 | .integrations.mt_base_url | |
| 217 | 220 | .as_ref() | |
| 218 | 221 | .map(|base| format!("{}/p/{}", base, db_project.slug)) | |
| @@ -224,8 +227,8 @@ pub(crate) async fn render_project_page( | |||
| 224 | 227 | .as_ref() | |
| 225 | 228 | .is_some_and(|u| u.id == db_project.user_id); | |
| 226 | 229 | ||
| 227 | - | let db_sections = db::project_sections::list_by_project(&state.db, db_project.id).await?; | |
| 228 | - | let cdn_base = state.config.cdn_base_url.as_deref().unwrap_or("https://cdn.makenot.work"); | |
| 230 | + | let db_sections = db::project_sections::list_by_project(db, db_project.id).await?; | |
| 231 | + | let cdn_base = config.cdn_base_url.as_deref().unwrap_or("https://cdn.makenot.work"); | |
| 229 | 232 | let sections: Vec<ProjectSection> = db_sections | |
| 230 | 233 | .iter() | |
| 231 | 234 | .map(|s| ProjectSection::from_db(s, db_project.user_id, cdn_base)) | |
| @@ -234,7 +237,7 @@ pub(crate) async fn render_project_page( | |||
| 234 | 237 | // Ordered gallery → carousel frames (additive to the cover image). Alt is | |
| 235 | 238 | // creator-optional; fall back to a title-based description (CarouselFrame::new | |
| 236 | 239 | // debug-asserts non-empty alt, so build the struct directly). | |
| 237 | - | let gallery = db::gallery_images::list_for_project(&state.db, db_project.id) | |
| 240 | + | let gallery = db::gallery_images::list_for_project(db, db_project.id) | |
| 238 | 241 | .await | |
| 239 | 242 | .unwrap_or_else(|e| { | |
| 240 | 243 | tracing::warn!(project_id = %db_project.id, error = %e, "gallery image lookup failed; rendering without carousel"); | |
| @@ -263,7 +266,7 @@ pub(crate) async fn render_project_page( | |||
| 263 | 266 | follower_count, | |
| 264 | 267 | subscription_tiers, | |
| 265 | 268 | has_subscription, | |
| 266 | - | host_url: state.config.host_url.clone(), | |
| 269 | + | host_url: config.host_url.clone(), | |
| 267 | 270 | git_repos, | |
| 268 | 271 | has_blog_posts, | |
| 269 | 272 | community_url, |
| @@ -19,7 +19,7 @@ use crate::{ | |||
| 19 | 19 | routes::custom_domain, | |
| 20 | 20 | templates::*, | |
| 21 | 21 | types::*, | |
| 22 | - | AppState, Billing, | |
| 22 | + | AppCaches, Billing, Integrations, | |
| 23 | 23 | }; | |
| 24 | 24 | ||
| 25 | 25 | /// Render the landing page, or redirect authenticated users to the library. | |
| @@ -28,15 +28,20 @@ use crate::{ | |||
| 28 | 28 | /// profile instead (the fallback handler only catches paths that don't match | |
| 29 | 29 | /// any named route, so `/` needs to be handled here). | |
| 30 | 30 | #[tracing::instrument(skip_all, name = "landing::index")] | |
| 31 | + | #[allow(clippy::too_many_arguments)] | |
| 31 | 32 | pub(super) async fn index( | |
| 32 | - | State(state): State<AppState>, | |
| 33 | + | State(db): State<PgPool>, | |
| 34 | + | State(caches): State<AppCaches>, | |
| 35 | + | State(integrations): State<Integrations>, | |
| 36 | + | State(config): State<Config>, | |
| 37 | + | State(billing): State<Billing>, | |
| 33 | 38 | headers: HeaderMap, | |
| 34 | 39 | session: Session, | |
| 35 | 40 | MaybeUserUnverified(maybe_user): MaybeUserUnverified, | |
| 36 | 41 | ) -> Result<Response> { | |
| 37 | 42 | // Check for custom domain — delegate to the custom domain handler | |
| 38 | 43 | if let Some(response) = | |
| 39 | - | custom_domain::try_handle(&state, &headers, "/", &session, &maybe_user).await | |
| 44 | + | custom_domain::try_handle(&db, &caches, &integrations, &config, &headers, "/", &session, &maybe_user).await | |
| 40 | 45 | { | |
| 41 | 46 | return Ok(response); | |
| 42 | 47 | } | |
| @@ -44,15 +49,15 @@ pub(super) async fn index( | |||
| 44 | 49 | match maybe_user { | |
| 45 | 50 | Some(_) => Ok(Redirect::to("/library").into_response()), | |
| 46 | 51 | None => { | |
| 47 | - | let total_creators = db::waitlist::count_active_creators(&state.db).await? as u32; | |
| 48 | - | let total_items = db::items::count_public_listed(&state.db).await?; | |
| 52 | + | let total_creators = db::waitlist::count_active_creators(&db).await? as u32; | |
| 53 | + | let total_items = db::items::count_public_listed(&db).await?; | |
| 49 | 54 | ||
| 50 | 55 | // "Last shipped" velocity line: most recent published, landing- | |
| 51 | 56 | // flagged post on the changelog project. Read once per render; the | |
| 52 | 57 | // line is suppressed entirely when nothing qualifies (no | |
| 53 | 58 | // placeholder), matching the runway disclosure's no-fabrication rule. | |
| 54 | 59 | let last_shipped = db::blog_posts::get_landing_changelog_post( | |
| 55 | - | &state.db, | |
| 60 | + | &db, | |
| 56 | 61 | constants::CHANGELOG_PROJECT_SLUG, | |
| 57 | 62 | ) | |
| 58 | 63 | .await? | |
| @@ -67,7 +72,7 @@ pub(super) async fn index( | |||
| 67 | 72 | // Surface remaining founder slots only when close enough to feel | |
| 68 | 73 | // scarce. 200 is "last chunk" — enough warning to convert, not so | |
| 69 | 74 | // early that the number stays prominent for months. | |
| 70 | - | let founder_window_open = state.config.creator_pricing.founder_window_open; | |
| 75 | + | let founder_window_open = config.creator_pricing.founder_window_open; | |
| 71 | 76 | const FOUNDER_CAP: u32 = 1_000; | |
| 72 | 77 | const URGENCY_THRESHOLD: u32 = 200; | |
| 73 | 78 | let founder_slots_remaining = if founder_window_open && total_creators >= FOUNDER_CAP.saturating_sub(URGENCY_THRESHOLD) { | |
| @@ -100,12 +105,12 @@ pub(super) async fn index( | |||
| 100 | 105 | ||
| 101 | 106 | Ok(IndexTemplate { | |
| 102 | 107 | csrf_token: get_csrf_token(&session).await, | |
| 103 | - | host_url: state.config.host_url.clone(), | |
| 108 | + | host_url: config.host_url.clone(), | |
| 104 | 109 | total_creators, | |
| 105 | 110 | total_items: total_items as u32, | |
| 106 | 111 | founder_window_open, | |
| 107 | 112 | founder_slots_remaining, | |
| 108 | - | tier_prices: state.tier_prices.clone(), | |
| 113 | + | tier_prices: billing.tier_prices.clone(), | |
| 109 | 114 | landing_carousel, | |
| 110 | 115 | last_shipped, | |
| 111 | 116 | }.into_response()) |