Skip to main content

max / makenotwork

10.4 KB · 345 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 UserSshKeysTabTemplate,
228 UserPaymentsTabTemplate,
229 UserProjectsTabTemplate,
230 UserCreatorTabTemplate,
231 ProjectOverviewTabTemplate,
232 ProjectContentTabTemplate,
233 ProjectAnalyticsTabTemplate,
234 UserAnalyticsTabTemplate,
235 BuyerContactsPartialTemplate,
236 PayoutSummaryPartialTemplate,
237 ProjectSettingsTabTemplate,
238 ProjectCodeTabTemplate,
239 ProjectBlogTabTemplate,
240 ProjectSubscriptionsTabTemplate,
241 ProjectMembersTabTemplate,
242 ProjectMonetizationTabTemplate,
243 ItemEditRowTemplate,
244 // Admin partials
245 AdminWaitlistEntriesTemplate,
246 AdminCompCodesEntriesTemplate,
247 AdminUserEntriesTemplate,
248 AdminUploadEntriesTemplate,
249 AdminQueueSummaryTemplate,
250 AdminAppealEntriesTemplate,
251 AdminReportEntriesTemplate,
252 SuspensionBannerTemplate,
253 // License keys
254 ItemLicenseKeysTemplate,
255 // Promo codes
256 PromoCodesListTemplate,
257 ProjectPromotionsTabTemplate,
258 // Sessions
259 UserSessionsPartialTemplate,
260 // SyncKit
261 UserSyncKitTabTemplate,
262 ProjectSyncKitTabTemplate,
263 // Forums (Multithreaded)
264 UserForumsTabTemplate,
265 // Media library
266 UserMediaTabTemplate,
267 // Support
268 UserSupportTabTemplate,
269 // Collections
270 CollectionTemplate,
271 // Library tabs
272 LibraryPurchasesTabTemplate,
273 LibraryFeedTabTemplate,
274 LibraryCollectionsTabTemplate,
275 LibraryContactsTabTemplate,
276 LibraryCommunitiesTabTemplate,
277 // Follow button
278 FollowButtonTemplate,
279 TagFollowToggleTemplate,
280 // Tag suggestions
281 TagSuggestionsTemplate,
282 // Item analytics
283 ItemAnalyticsPartialTemplate,
284 // Item dashboard tabs
285 ItemOverviewTabTemplate,
286 ItemDetailsTabTemplate,
287 ItemPricingTabTemplate,
288 ItemFilesTabTemplate,
289 ItemSalesTabTemplate,
290 ItemEmbedTabTemplate,
291 // Onboarding checklist
292 OnboardingChecklistPartialTemplate,
293 // Tag tree browser
294 TagTreeTemplate,
295 // TOTP 2FA
296 TotpSetupTemplate,
297 TotpStatusTemplate,
298 // Passkeys
299 PasskeyListTemplate,
300 // Git source browser
301 GitRepoTemplate,
302 GitTreeTemplate,
303 GitFileTemplate,
304 GitCommitsTemplate,
305 GitCommitDetailTemplate,
306 GitNotesTemplate,
307 GitTagsTemplate,
308 GitReplaceTemplate,
309 GitBlameTemplate,
310 GitUserReposTemplate,
311 GitExploreTemplate,
312 GitFileLogTemplate,
313 // Git issues
314 GitIssueListTemplate,
315 GitIssueDetailTemplate,
316 GitRepoSettingsTemplate,
317 // Join wizard
318 WizardJoinTemplate,
319 WizardJoinAccountTemplate,
320 WizardJoinProfileTemplate,
321 WizardJoinCompleteTemplate,
322 // Creation wizards, full pages
323 WizardProjectTemplate,
324 WizardItemTemplate,
325 // Creation wizards, project step partials
326 WizardProjectBasicsTemplate,
327 WizardProjectAppearanceTemplate,
328 WizardProjectMonetizationTemplate,
329 WizardProjectFirstContentTemplate,
330 WizardProjectPreviewTemplate,
331 // Creation wizards, item step partials
332 WizardItemTypeTemplate,
333 WizardItemBasicsTemplate,
334 WizardItemContentTemplate,
335 WizardItemSectionsTemplate,
336 WizardItemPricingTemplate,
337 WizardItemPreviewTemplate,
338 // Embed widgets
339 EmbedItemButtonTemplate,
340 EmbedItemCardTemplate,
341 EmbedItemPlayerTemplate,
342 EmbedTipButtonTemplate,
343 EmbedProjectCardTemplate,
344 );
345