Skip to main content

max / makenotwork

9.7 KB · 321 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 StripeConnectDisclaimerTemplate,
171 // Blog pages
172 ProjectBlogTemplate,
173 BlogPostTemplate,
174 // Documentation pages
175 DocTemplate,
176 DocIndexTemplate,
177 // Platform economics + runway disclosure
178 EconomicsTemplate,
179 // Use cases
180 UseCasesTemplate,
181 // Team
182 TeamTemplate,
183 // Fan+
184 FanPlusTemplate,
185 // Creator invite system
186 CreatorsTemplate,
187 // Email & account
188 EmailResultTemplate,
189 EmailPreferencesTemplate,
190 ConfirmDeleteTemplate,
191 AccountDeletedTemplate,
192 AcknowledgeTemplate,
193 // Health
194 HealthTemplate,
195 // Dashboard pages
196 DashboardUserTemplate,
197 DashboardProjectTemplate,
198 DashboardItemTemplate,
199 // Admin
200 AdminWaitlistTemplate,
201 AdminUsersTemplate,
202 AdminUploadsTemplate,
203 AdminScanAuditTemplate,
204 AdminAppealsTemplate,
205 AdminReportsTemplate,
206 AdminSignupsTemplate,
207 AdminMailCapsTemplate,
208 AdminMailCapEntriesTemplate,
209 AdminMetricsTemplate,
210 AdminCompCodesTemplate,
211 // Export, import & account management
212 ExportPortalTemplate,
213 ImportPortalTemplate,
214 DeleteAccountTemplate,
215 BlogEditorTemplate,
216 // HTMX partials
217 AlertTemplate,
218 LibraryStatusTemplate,
219 ExportDownloadTemplate,
220 ExportContentReadyTemplate,
221 TransactionsTableTemplate,
222 UserProfileTabTemplate,
223 UserSettingsTabTemplate,
224 UserAccountTabTemplate,
225 UserPaymentsTabTemplate,
226 UserCreatorTabTemplate,
227 ProjectSettingsTabTemplate,
228 ProjectCodeTabTemplate,
229 ProjectSubscriptionsTabTemplate,
230 ProjectMonetizationTabTemplate,
231 ItemEditRowTemplate,
232 // Admin partials
233 AdminWaitlistEntriesTemplate,
234 AdminCompCodesEntriesTemplate,
235 AdminUserEntriesTemplate,
236 AdminUploadEntriesTemplate,
237 AdminQueueSummaryTemplate,
238 AdminAppealEntriesTemplate,
239 AdminReportEntriesTemplate,
240 SuspensionBannerTemplate,
241 // License keys
242 ItemLicenseKeysTemplate,
243 // Promo codes
244 PromoCodesListTemplate,
245 // Sessions
246 UserSessionsPartialTemplate,
247 // SyncKit
248 UserSyncKitTabTemplate,
249 ProjectSyncKitTabTemplate,
250 // Forums (Multithreaded)
251 // Media library
252 UserMediaTabTemplate,
253 // Support
254 // Collections
255 CollectionTemplate,
256 // Library tabs
257 LibraryPurchasesTabTemplate,
258 LibraryCollectionsTabTemplate,
259 // Follow button
260 FollowButtonTemplate,
261 TagFollowToggleTemplate,
262 // Tag suggestions
263 TagSuggestionsTemplate,
264 // Item analytics
265 ItemAnalyticsPartialTemplate,
266 // Item dashboard tabs
267 ItemOverviewTabTemplate,
268 ItemDetailsTabTemplate,
269 ItemPricingTabTemplate,
270 ItemVersionUploadTemplate,
271 ItemEmbedTabTemplate,
272 // Onboarding checklist
273 OnboardingChecklistPartialTemplate,
274 // Tag tree browser
275 TagTreeTemplate,
276 // TOTP 2FA
277 TotpSetupTemplate,
278 TotpStatusTemplate,
279 // Passkeys
280 PasskeyListTemplate,
281 // Git source browser
282 GitRepoTemplate,
283 GitTreeTemplate,
284 GitFileTemplate,
285 GitCommitsTemplate,
286 GitCommitDetailTemplate,
287 GitNotesTemplate,
288 GitAnnotationsTemplate,
289 GitTagsTemplate,
290 GitReplaceTemplate,
291 GitBlameTemplate,
292 GitUserReposTemplate,
293 GitExploreTemplate,
294 GitFileLogTemplate,
295 // Git issues
296 GitIssueListTemplate,
297 GitIssueDetailTemplate,
298 GitRepoSettingsTemplate,
299 // Join wizard
300 WizardJoinTemplate,
301 WizardJoinAccountTemplate,
302 WizardJoinProfileTemplate,
303 WizardJoinCompleteTemplate,
304 // Creation wizards, full pages
305 WizardProjectTemplate,
306 WizardItemTemplate,
307 // Creation wizards, project step partials
308 WizardProjectBasicsTemplate,
309 WizardProjectAppearanceTemplate,
310 WizardProjectMonetizationTemplate,
311 WizardProjectFirstContentTemplate,
312 WizardProjectPreviewTemplate,
313 // Creation wizards, item step partials
314 WizardItemTypeTemplate,
315 WizardItemBasicsTemplate,
316 WizardItemContentTemplate,
317 WizardItemSectionsTemplate,
318 WizardItemPricingTemplate,
319 WizardItemPreviewTemplate,
320 );
321