Skip to main content

max / makenotwork

9.7 KB · 323 lines History Blame Raw
1 //! Askama template definitions for all HTML pages and fragments.
2 //!
3 //! Split by domain:
4 //! - `public`: landing, auth, content, blog, discover, health
5 //! - `dashboard`: creator dashboards, admin, export, account management
6 //! - `partials`: HTMX fragments, tab content, alerts, form status
7
8 mod dashboard;
9 mod embed;
10 mod partials;
11 mod public;
12
13 pub use dashboard::*;
14 pub use embed::*;
15 pub use partials::*;
16 pub use public::*;
17
18 use askama::Template;
19 use axum::{
20 http::StatusCode,
21 response::{Html, IntoResponse, Response},
22 };
23
24 /// Base context shared by all templates.
25 /// Note: csrf_token is Option to allow templates to work without CSRF
26 /// but all authenticated pages should include it.
27 pub type CsrfTokenOption = Option<String>;
28
29 include!(concat!(env!("OUT_DIR"), "/shot_dimensions.rs"));
30
31 /// The picture's own size, for a static asset this build read off disk.
32 ///
33 /// `None` for anything not in the generated table -- a creator's gallery upload,
34 /// whose dimensions the database never recorded. That is honest: the renderer
35 /// reserves nothing rather than reserving the wrong thing.
36 #[must_use]
37 pub fn shot_size(path: &str) -> Option<(u32, u32)> {
38 SHOT_DIMENSIONS
39 .iter()
40 .find(|(p, _, _)| *p == path)
41 .map(|&(_, w, h)| (w, h))
42 }
43
44 /// One frame of the composable click-through carousel (`partials/carousel.html`).
45 ///
46 /// A carousel is just an ordered `&[CarouselFrame]`; the same macro renders it
47 /// on any surface (app product pages, landing), only the frame list differs.
48 /// Build a `Vec<CarouselFrame>` on a page template and pass it to the macro.
49 ///
50 /// Prefer [`CarouselFrame::new`] over a struct literal: it makes the alt text a
51 /// required, named argument and nudges (in debug builds) toward alt that
52 /// actually describes the screenshot. A carousel frame is a meaningful image,
53 /// so alt is not optional and should not be a filename or a bare label like
54 /// "screenshot". A screen-reader user should get the same information a
55 /// sighted viewer does.
56 #[derive(Clone)]
57 pub struct CarouselFrame {
58 /// Image URL (typically an optimized screenshot under `/static/images/shots/`).
59 pub image: String,
60 /// Alt text describing the screenshot. Required, every frame is an image.
61 pub alt: String,
62 /// Optional caption shown under the frame.
63 pub caption: Option<String>,
64 /// The image's own pixel dimensions, where this build could learn them.
65 ///
66 /// Static shots get theirs read off disk by build.rs. A creator's gallery
67 /// upload gets `None`, because `gallery_images` records a byte count and
68 /// never recorded a size.
69 ///
70 /// What it buys: the renderer writes `width`/`height`, the browser holds
71 /// the frame's place from first paint, and nothing below the carousel moves
72 /// when the picture lands.
73 pub intrinsic: Option<(u32, u32)>,
74 }
75
76 impl CarouselFrame {
77 /// Build a frame, nudging toward helpful alt text.
78 ///
79 /// In debug builds this asserts the alt text is non-empty and looks like a
80 /// description rather than a filename or a one-word placeholder. The checks
81 /// are debug-only so they guide authors during development without ever
82 /// affecting a release render.
83 pub fn new(image: impl Into<String>, alt: impl Into<String>) -> Self {
84 let image = image.into();
85 let alt = alt.into();
86 debug_assert!(
87 !alt.trim().is_empty(),
88 "carousel frame `{image}` has empty alt text, describe what the \
89 screenshot shows so screen-reader users get the same information \
90 sighted viewers do"
91 );
92 debug_assert!(
93 !alt.trim_start().starts_with('/') && !alt.contains(".webp") && !alt.contains(".png"),
94 "carousel frame alt text looks like a filename (`{alt}`), write a \
95 human description of what the screenshot shows instead"
96 );
97 Self {
98 // Looked up rather than passed, so every caller that names a static
99 // shot reserves its space without having to know it did. A path the
100 // table does not carry -- a creator upload -- stays None, which is
101 // the honest answer and not a fallback.
102 intrinsic: shot_size(&image),
103 image,
104 alt,
105 caption: None,
106 }
107 }
108
109 /// Attach an optional caption shown under the frame.
110 #[must_use]
111 pub fn with_caption(mut self, caption: impl Into<String>) -> Self {
112 self.caption = Some(caption.into());
113 self
114 }
115 }
116
117 /// Helper to convert any Askama template into an Axum response.
118 fn render_template<T: Template>(template: T) -> Response {
119 match template.render() {
120 Ok(html) => Html(html).into_response(),
121 Err(err) => {
122 tracing::error!(error = ?err, "template rendering error");
123 (StatusCode::INTERNAL_SERVER_ERROR, "Template error").into_response()
124 }
125 }
126 }
127
128 /// Implement `IntoResponse` for one or more Askama template structs.
129 macro_rules! impl_into_response {
130 ($($T:ty),+ $(,)?) => {
131 $(
132 impl IntoResponse for $T {
133 fn into_response(self) -> Response {
134 render_template(self)
135 }
136 }
137 )+
138 };
139 }
140
141 impl_into_response!(
142 // Public pages
143 SandboxTemplate,
144 PolicyTemplate,
145 IndexTemplate,
146 LibraryTemplate,
147 CartTemplate,
148 LoginTemplate,
149 TwoFactorTemplate,
150 OAuthAuthorizeTemplate,
151 ForgotPasswordTemplate,
152 ResetPasswordTemplate,
153 UserTemplate,
154 ProjectTemplate,
155 ProjectPaywallTemplate,
156 ItemTemplate,
157 LibraryAudioTemplate,
158 LibraryDownloadsTemplate,
159 LibraryLockedTemplate,
160 LibraryTextTemplate,
161 LibraryVideoTemplate,
162 TextReaderTemplate,
163 AudioPlayerTemplate,
164 VideoPlayerTemplate,
165 DiscoverTemplate,
166 DiscoverResultsTemplate,
167 PurchaseTemplate,
168 ReceiptTemplate,
169 BuyPageTemplate,
170 FeedTemplate,
171 StripeConnectDisclaimerTemplate,
172 // Blog pages
173 ProjectBlogTemplate,
174 BlogPostTemplate,
175 // Documentation pages
176 DocTemplate,
177 DocIndexTemplate,
178 // Pricing calculator
179 PricingTemplate,
180 FeeCalculatorPartial,
181 // Platform economics + runway disclosure
182 EconomicsTemplate,
183 // Use cases
184 UseCasesTemplate,
185 // Team
186 TeamTemplate,
187 // Fan+
188 FanPlusTemplate,
189 // Creator invite system
190 CreatorsTemplate,
191 // Email & account
192 EmailResultTemplate,
193 EmailPreferencesTemplate,
194 ConfirmDeleteTemplate,
195 AccountDeletedTemplate,
196 AcknowledgeTemplate,
197 // Health
198 HealthTemplate,
199 // Dashboard pages
200 DashboardUserTemplate,
201 DashboardProjectTemplate,
202 DashboardItemTemplate,
203 // Admin
204 AdminWaitlistTemplate,
205 AdminUsersTemplate,
206 AdminUploadsTemplate,
207 AdminScanAuditTemplate,
208 AdminAppealsTemplate,
209 AdminReportsTemplate,
210 AdminSignupsTemplate,
211 AdminMetricsTemplate,
212 AdminCompCodesTemplate,
213 // Export, import & account management
214 ExportPortalTemplate,
215 ImportPortalTemplate,
216 DeleteAccountTemplate,
217 BlogEditorTemplate,
218 // HTMX partials
219 AlertTemplate,
220 LibraryStatusTemplate,
221 ExportDownloadTemplate,
222 ExportContentReadyTemplate,
223 TransactionsTableTemplate,
224 UserProfileTabTemplate,
225 UserSettingsTabTemplate,
226 UserAccountTabTemplate,
227 UserPaymentsTabTemplate,
228 UserCreatorTabTemplate,
229 ProjectSettingsTabTemplate,
230 ProjectCodeTabTemplate,
231 ProjectSubscriptionsTabTemplate,
232 ProjectMonetizationTabTemplate,
233 ItemEditRowTemplate,
234 // Admin partials
235 AdminWaitlistEntriesTemplate,
236 AdminCompCodesEntriesTemplate,
237 AdminUserEntriesTemplate,
238 AdminUploadEntriesTemplate,
239 AdminQueueSummaryTemplate,
240 AdminAppealEntriesTemplate,
241 AdminReportEntriesTemplate,
242 SuspensionBannerTemplate,
243 // License keys
244 ItemLicenseKeysTemplate,
245 // Promo codes
246 PromoCodesListTemplate,
247 // Sessions
248 UserSessionsPartialTemplate,
249 // SyncKit
250 UserSyncKitTabTemplate,
251 ProjectSyncKitTabTemplate,
252 // Forums (Multithreaded)
253 // Media library
254 UserMediaTabTemplate,
255 // Support
256 // Collections
257 CollectionTemplate,
258 // Library tabs
259 LibraryPurchasesTabTemplate,
260 LibraryFeedTabTemplate,
261 LibraryCollectionsTabTemplate,
262 // Follow button
263 FollowButtonTemplate,
264 TagFollowToggleTemplate,
265 // Tag suggestions
266 TagSuggestionsTemplate,
267 // Item analytics
268 ItemAnalyticsPartialTemplate,
269 // Item dashboard tabs
270 ItemOverviewTabTemplate,
271 ItemDetailsTabTemplate,
272 ItemPricingTabTemplate,
273 ItemFilesTabTemplate,
274 ItemEmbedTabTemplate,
275 // Onboarding checklist
276 OnboardingChecklistPartialTemplate,
277 // Tag tree browser
278 TagTreeTemplate,
279 // TOTP 2FA
280 TotpSetupTemplate,
281 TotpStatusTemplate,
282 // Passkeys
283 PasskeyListTemplate,
284 // Git source browser
285 GitRepoTemplate,
286 GitTreeTemplate,
287 GitFileTemplate,
288 GitCommitsTemplate,
289 GitCommitDetailTemplate,
290 GitNotesTemplate,
291 GitTagsTemplate,
292 GitReplaceTemplate,
293 GitBlameTemplate,
294 GitUserReposTemplate,
295 GitExploreTemplate,
296 GitFileLogTemplate,
297 // Git issues
298 GitIssueListTemplate,
299 GitIssueDetailTemplate,
300 GitRepoSettingsTemplate,
301 // Join wizard
302 WizardJoinTemplate,
303 WizardJoinAccountTemplate,
304 WizardJoinProfileTemplate,
305 WizardJoinCompleteTemplate,
306 // Creation wizards, full pages
307 WizardProjectTemplate,
308 WizardItemTemplate,
309 // Creation wizards, project step partials
310 WizardProjectBasicsTemplate,
311 WizardProjectAppearanceTemplate,
312 WizardProjectMonetizationTemplate,
313 WizardProjectFirstContentTemplate,
314 WizardProjectPreviewTemplate,
315 // Creation wizards, item step partials
316 WizardItemTypeTemplate,
317 WizardItemBasicsTemplate,
318 WizardItemContentTemplate,
319 WizardItemSectionsTemplate,
320 WizardItemPricingTemplate,
321 WizardItemPreviewTemplate,
322 );
323