Skip to main content

max / makenotwork

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