Skip to main content

max / makenotwork

9.8 KB · 325 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 AdminMailCapsTemplate,
212 AdminMailCapEntriesTemplate,
213 AdminMetricsTemplate,
214 AdminCompCodesTemplate,
215 // Export, import & account management
216 ExportPortalTemplate,
217 ImportPortalTemplate,
218 DeleteAccountTemplate,
219 BlogEditorTemplate,
220 // HTMX partials
221 AlertTemplate,
222 LibraryStatusTemplate,
223 ExportDownloadTemplate,
224 ExportContentReadyTemplate,
225 TransactionsTableTemplate,
226 UserProfileTabTemplate,
227 UserSettingsTabTemplate,
228 UserAccountTabTemplate,
229 UserPaymentsTabTemplate,
230 UserCreatorTabTemplate,
231 ProjectSettingsTabTemplate,
232 ProjectCodeTabTemplate,
233 ProjectSubscriptionsTabTemplate,
234 ProjectMonetizationTabTemplate,
235 ItemEditRowTemplate,
236 // Admin partials
237 AdminWaitlistEntriesTemplate,
238 AdminCompCodesEntriesTemplate,
239 AdminUserEntriesTemplate,
240 AdminUploadEntriesTemplate,
241 AdminQueueSummaryTemplate,
242 AdminAppealEntriesTemplate,
243 AdminReportEntriesTemplate,
244 SuspensionBannerTemplate,
245 // License keys
246 ItemLicenseKeysTemplate,
247 // Promo codes
248 PromoCodesListTemplate,
249 // Sessions
250 UserSessionsPartialTemplate,
251 // SyncKit
252 UserSyncKitTabTemplate,
253 ProjectSyncKitTabTemplate,
254 // Forums (Multithreaded)
255 // Media library
256 UserMediaTabTemplate,
257 // Support
258 // Collections
259 CollectionTemplate,
260 // Library tabs
261 LibraryPurchasesTabTemplate,
262 LibraryFeedTabTemplate,
263 LibraryCollectionsTabTemplate,
264 // Follow button
265 FollowButtonTemplate,
266 TagFollowToggleTemplate,
267 // Tag suggestions
268 TagSuggestionsTemplate,
269 // Item analytics
270 ItemAnalyticsPartialTemplate,
271 // Item dashboard tabs
272 ItemOverviewTabTemplate,
273 ItemDetailsTabTemplate,
274 ItemPricingTabTemplate,
275 ItemFilesTabTemplate,
276 ItemEmbedTabTemplate,
277 // Onboarding checklist
278 OnboardingChecklistPartialTemplate,
279 // Tag tree browser
280 TagTreeTemplate,
281 // TOTP 2FA
282 TotpSetupTemplate,
283 TotpStatusTemplate,
284 // Passkeys
285 PasskeyListTemplate,
286 // Git source browser
287 GitRepoTemplate,
288 GitTreeTemplate,
289 GitFileTemplate,
290 GitCommitsTemplate,
291 GitCommitDetailTemplate,
292 GitNotesTemplate,
293 GitTagsTemplate,
294 GitReplaceTemplate,
295 GitBlameTemplate,
296 GitUserReposTemplate,
297 GitExploreTemplate,
298 GitFileLogTemplate,
299 // Git issues
300 GitIssueListTemplate,
301 GitIssueDetailTemplate,
302 GitRepoSettingsTemplate,
303 // Join wizard
304 WizardJoinTemplate,
305 WizardJoinAccountTemplate,
306 WizardJoinProfileTemplate,
307 WizardJoinCompleteTemplate,
308 // Creation wizards, full pages
309 WizardProjectTemplate,
310 WizardItemTemplate,
311 // Creation wizards, project step partials
312 WizardProjectBasicsTemplate,
313 WizardProjectAppearanceTemplate,
314 WizardProjectMonetizationTemplate,
315 WizardProjectFirstContentTemplate,
316 WizardProjectPreviewTemplate,
317 // Creation wizards, item step partials
318 WizardItemTypeTemplate,
319 WizardItemBasicsTemplate,
320 WizardItemContentTemplate,
321 WizardItemSectionsTemplate,
322 WizardItemPricingTemplate,
323 WizardItemPreviewTemplate,
324 );
325