Skip to main content

max / multithreaded

13.1 KB · 472 lines History Blame Raw
1 //! Templates for public-facing forum pages.
2
3 use askama::Template;
4
5 use super::CsrfTokenOption;
6
7 // ============================================================================
8 // View-model structs (lightweight data for templates, not domain models)
9 // ============================================================================
10
11 /// Minimal user info for the site header.
12 pub struct TemplateSessionUser {
13 pub username: String,
14 pub is_platform_admin: bool,
15 }
16
17 /// Row in the forum directory (home page).
18 pub struct CommunityDirectoryRow {
19 pub slug: String,
20 pub name: String,
21 pub description: Option<String>,
22 pub category_count: u32,
23 pub thread_count: u32,
24 }
25
26 /// Row in the project detail page (category listing).
27 pub struct CategoryRow {
28 pub name: String,
29 pub slug: String,
30 pub description: Option<String>,
31 pub thread_count: u32,
32 }
33
34 /// Tag badge for display in templates.
35 pub struct TagBadge {
36 pub id: String,
37 pub name: String,
38 pub slug: String,
39 }
40
41 /// Row in the dense thread table.
42 pub struct ThreadRow {
43 pub id: String,
44 pub title: String,
45 pub author_name: String,
46 pub author_username: String,
47 pub reply_count: u32,
48 pub last_activity: String,
49 pub pinned: bool,
50 pub locked: bool,
51 pub has_mention: bool,
52 pub tags: Vec<TagBadge>,
53 }
54
55 /// Footnote on a post.
56 pub struct FootnoteViewRow {
57 pub author_name: String,
58 pub body_html: String,
59 pub timestamp: String,
60 }
61
62 /// Link preview card for a post.
63 pub struct LinkPreviewViewRow {
64 pub url: String,
65 pub title: Option<String>,
66 pub description: Option<String>,
67 }
68
69 /// Single post in a thread.
70 pub struct PostRow {
71 pub id: String,
72 pub author_name: String,
73 pub author_username: String,
74 pub timestamp: String,
75 pub body_html: String,
76 pub is_op: bool,
77 pub is_removed: bool,
78 pub can_add_footnote: bool,
79 pub can_remove: bool,
80 pub can_flag: bool,
81 pub footnotes: Vec<FootnoteViewRow>,
82 pub link_previews: Vec<LinkPreviewViewRow>,
83 pub endorsement_count: u32,
84 pub is_endorsed: bool,
85 pub can_endorse: bool,
86 }
87
88 // ============================================================================
89 // Pagination
90 // ============================================================================
91
92 /// Pagination state passed to templates.
93 pub struct Pagination {
94 pub current_page: u32,
95 pub total_pages: u32,
96 pub has_prev: bool,
97 pub has_next: bool,
98 }
99
100 impl Pagination {
101 pub fn new(page: u32, total_items: i64, per_page: i64) -> Self {
102 let total_pages = ((total_items as f64) / (per_page as f64)).ceil() as u32;
103 let total_pages = total_pages.max(1);
104 let current_page = page.min(total_pages);
105 Self {
106 current_page,
107 total_pages,
108 has_prev: current_page > 1,
109 has_next: current_page < total_pages,
110 }
111 }
112 }
113
114 // ============================================================================
115 // Page templates
116 // ============================================================================
117
118 /// Forum directory — lists local communities.
119 #[derive(Template)]
120 #[template(path = "pages/forum_directory.html")]
121 pub struct ForumDirectoryTemplate {
122 pub csrf_token: CsrfTokenOption,
123 pub session_user: Option<TemplateSessionUser>,
124 pub mnw_base_url: std::sync::Arc<str>,
125 pub communities: Vec<CommunityDirectoryRow>,
126 pub pagination: Pagination,
127 }
128
129 /// Project forum — category table within a single project.
130 #[derive(Template)]
131 #[template(path = "pages/community.html")]
132 pub struct CommunityTemplate {
133 pub csrf_token: CsrfTokenOption,
134 pub session_user: Option<TemplateSessionUser>,
135 pub mnw_base_url: std::sync::Arc<str>,
136 pub community_name: String,
137 pub community_slug: String,
138 pub community_description: Option<String>,
139 pub categories: Vec<CategoryRow>,
140 pub is_owner: bool,
141 pub is_mod_or_owner: bool,
142 }
143
144 /// Category view — dense thread table (the signature UI).
145 #[derive(Template)]
146 #[template(path = "pages/category.html")]
147 pub struct CategoryTemplate {
148 pub csrf_token: CsrfTokenOption,
149 pub session_user: Option<TemplateSessionUser>,
150 pub mnw_base_url: std::sync::Arc<str>,
151 pub community_name: String,
152 pub community_slug: String,
153 pub category_name: String,
154 pub category_slug: String,
155 pub threads: Vec<ThreadRow>,
156 pub pagination: Pagination,
157 pub sort_column: String,
158 pub sort_order: String,
159 pub available_tags: Vec<TagBadge>,
160 pub active_tag: Option<String>,
161 }
162
163 /// Thread view — post list with reply form.
164 #[derive(Template)]
165 #[template(path = "pages/thread.html")]
166 pub struct ThreadTemplate {
167 pub csrf_token: CsrfTokenOption,
168 pub session_user: Option<TemplateSessionUser>,
169 pub mnw_base_url: std::sync::Arc<str>,
170 pub community_name: String,
171 pub community_slug: String,
172 pub category_name: String,
173 pub category_slug: String,
174 pub thread_id: String,
175 pub thread_title: String,
176 pub locked: bool,
177 pub pinned: bool,
178 pub is_mod: bool,
179 pub can_mod_thread: bool,
180 pub is_tracked: bool,
181 pub posts: Vec<PostRow>,
182 pub pagination: Pagination,
183 }
184
185 /// New thread creation form.
186 #[derive(Template)]
187 #[template(path = "pages/new_thread.html")]
188 pub struct NewThreadTemplate {
189 pub csrf_token: CsrfTokenOption,
190 pub session_user: Option<TemplateSessionUser>,
191 pub mnw_base_url: std::sync::Arc<str>,
192 pub community_name: String,
193 pub community_slug: String,
194 pub category_name: String,
195 pub category_slug: String,
196 pub available_tags: Vec<TagBadge>,
197 }
198
199 /// Edit thread title form.
200 #[derive(Template)]
201 #[template(path = "pages/edit_thread.html")]
202 pub struct EditThreadTemplate {
203 pub csrf_token: CsrfTokenOption,
204 pub session_user: Option<TemplateSessionUser>,
205 pub mnw_base_url: std::sync::Arc<str>,
206 pub community_name: String,
207 pub community_slug: String,
208 pub category_name: String,
209 pub category_slug: String,
210 pub thread_id: String,
211 pub current_title: String,
212 }
213
214 /// Category row for the settings page.
215 pub struct SettingsCategoryRow {
216 pub id: String,
217 pub name: String,
218 pub slug: String,
219 pub description: Option<String>,
220 pub sort_order: i32,
221 pub is_first: bool,
222 pub is_last: bool,
223 }
224
225 /// Community settings page (owner only).
226 #[derive(Template)]
227 #[template(path = "pages/community_settings.html")]
228 pub struct CommunitySettingsTemplate {
229 pub csrf_token: CsrfTokenOption,
230 pub session_user: Option<TemplateSessionUser>,
231 pub mnw_base_url: std::sync::Arc<str>,
232 pub community_name: String,
233 pub community_slug: String,
234 pub community_description: Option<String>,
235 pub auto_hide_threshold: Option<i32>,
236 pub categories: Vec<SettingsCategoryRow>,
237 pub tags: Vec<TagBadge>,
238 }
239
240 /// Edit category form (owner only).
241 #[derive(Template)]
242 #[template(path = "pages/edit_category.html")]
243 pub struct EditCategoryTemplate {
244 pub csrf_token: CsrfTokenOption,
245 pub session_user: Option<TemplateSessionUser>,
246 pub mnw_base_url: std::sync::Arc<str>,
247 pub community_name: String,
248 pub community_slug: String,
249 pub category_id: String,
250 pub category_name: String,
251 pub category_description: Option<String>,
252 }
253
254 /// Row in the member list.
255 pub struct MemberListRow {
256 pub username: String,
257 pub display_name: String,
258 pub role: String,
259 pub joined: String,
260 }
261
262 /// Community member list page.
263 #[derive(Template)]
264 #[template(path = "pages/members.html")]
265 pub struct MembersTemplate {
266 pub csrf_token: CsrfTokenOption,
267 pub session_user: Option<TemplateSessionUser>,
268 pub mnw_base_url: std::sync::Arc<str>,
269 pub community_name: String,
270 pub community_slug: String,
271 pub members: Vec<MemberListRow>,
272 }
273
274 /// Activity row for user profile page.
275 pub struct ProfileActivityRow {
276 pub thread_id: String,
277 pub thread_title: String,
278 pub category_name: String,
279 pub category_slug: String,
280 pub timestamp: String,
281 pub is_thread_author: bool,
282 }
283
284 /// User profile within a community.
285 #[derive(Template)]
286 #[template(path = "pages/user_profile.html")]
287 pub struct UserProfileTemplate {
288 pub csrf_token: CsrfTokenOption,
289 pub session_user: Option<TemplateSessionUser>,
290 pub mnw_base_url: std::sync::Arc<str>,
291 pub community_name: String,
292 pub community_slug: String,
293 pub username: String,
294 pub display_name: String,
295 pub avatar_url: Option<String>,
296 pub role: String,
297 pub joined: String,
298 pub post_count: i64,
299 pub endorsement_count: i64,
300 pub activity: Vec<ProfileActivityRow>,
301 }
302
303 /// Row in tracked threads list.
304 pub struct TrackedThreadViewRow {
305 pub thread_id: String,
306 pub thread_title: String,
307 pub community_name: String,
308 pub community_slug: String,
309 pub category_slug: String,
310 pub unread_count: u32,
311 pub has_mention: bool,
312 }
313
314 /// Tracked threads page.
315 #[derive(Template)]
316 #[template(path = "pages/tracked.html")]
317 pub struct TrackedThreadsTemplate {
318 pub csrf_token: CsrfTokenOption,
319 pub session_user: Option<TemplateSessionUser>,
320 pub mnw_base_url: std::sync::Arc<str>,
321 pub threads: Vec<TrackedThreadViewRow>,
322 }
323
324 // ============================================================================
325 // Search templates
326 // ============================================================================
327
328 /// Single search result row.
329 pub struct SearchResultViewRow {
330 pub thread_id: String,
331 pub thread_title: String,
332 pub author_username: String,
333 pub community_name: String,
334 pub community_slug: String,
335 pub category_name: String,
336 pub category_slug: String,
337 pub snippet: String,
338 pub last_activity: String,
339 }
340
341 /// HTMX fragment — search results list.
342 #[derive(Template)]
343 #[template(path = "fragments/search_results.html")]
344 pub struct SearchResultsFragment {
345 pub results: Vec<SearchResultViewRow>,
346 }
347
348 /// Privacy/tracking info page.
349 #[derive(Template)]
350 #[template(path = "pages/tracking_info.html")]
351 pub struct TrackingInfoTemplate {
352 pub csrf_token: CsrfTokenOption,
353 pub session_user: Option<TemplateSessionUser>,
354 pub mnw_base_url: std::sync::Arc<str>,
355 }
356
357 /// 404 error page.
358 #[derive(Template)]
359 #[template(path = "pages/error_404.html")]
360 pub struct Error404Template {
361 pub csrf_token: CsrfTokenOption,
362 pub session_user: Option<TemplateSessionUser>,
363 pub mnw_base_url: std::sync::Arc<str>,
364 }
365
366 /// 500 error page.
367 #[derive(Template)]
368 #[template(path = "pages/error_500.html")]
369 pub struct Error500Template {
370 pub csrf_token: CsrfTokenOption,
371 pub session_user: Option<TemplateSessionUser>,
372 pub mnw_base_url: std::sync::Arc<str>,
373 }
374
375 // ============================================================================
376 // Moderation templates
377 // ============================================================================
378
379 /// Row in the active bans/mutes table.
380 pub struct BanListRow {
381 pub username: String,
382 pub display_name: Option<String>,
383 pub ban_type: String,
384 pub reason: Option<String>,
385 pub expires: Option<String>,
386 pub created: String,
387 pub banned_by: String,
388 }
389
390 /// Row in the mod log.
391 pub struct ModLogRow {
392 pub actor: String,
393 pub action: String,
394 pub target: Option<String>,
395 pub reason: Option<String>,
396 pub timestamp: String,
397 }
398
399 /// Pending flag for moderation page.
400 pub struct FlagViewRow {
401 pub flag_id: String,
402 pub post_id: String,
403 pub thread_id: String,
404 pub thread_title: String,
405 pub category_slug: String,
406 pub flagger_username: String,
407 pub reason: String,
408 pub detail: Option<String>,
409 pub created: String,
410 }
411
412 /// Community moderation page (mod/owner only).
413 #[derive(Template)]
414 #[template(path = "pages/moderation.html")]
415 pub struct ModerationTemplate {
416 pub csrf_token: CsrfTokenOption,
417 pub session_user: Option<TemplateSessionUser>,
418 pub mnw_base_url: std::sync::Arc<str>,
419 pub community_name: String,
420 pub community_slug: String,
421 pub bans: Vec<BanListRow>,
422 pub pending_flags: Vec<FlagViewRow>,
423 pub is_owner: bool,
424 }
425
426 /// Mod log page (mod/owner only).
427 #[derive(Template)]
428 #[template(path = "pages/mod_log.html")]
429 pub struct ModLogTemplate {
430 pub csrf_token: CsrfTokenOption,
431 pub session_user: Option<TemplateSessionUser>,
432 pub mnw_base_url: std::sync::Arc<str>,
433 pub community_name: String,
434 pub community_slug: String,
435 pub entries: Vec<ModLogRow>,
436 pub pagination: Pagination,
437 }
438
439 // ============================================================================
440 // Admin templates
441 // ============================================================================
442
443 /// Row for communities in admin dashboard.
444 pub struct AdminCommunityViewRow {
445 pub id: String,
446 pub name: String,
447 pub slug: String,
448 pub is_suspended: bool,
449 pub suspension_reason: Option<String>,
450 }
451
452 /// Row for users in admin dashboard.
453 pub struct AdminUserViewRow {
454 pub id: String,
455 pub username: String,
456 pub display_name: Option<String>,
457 pub is_suspended: bool,
458 pub suspension_reason: Option<String>,
459 }
460
461 /// Platform admin dashboard.
462 #[derive(Template)]
463 #[template(path = "pages/admin.html")]
464 pub struct AdminDashboardTemplate {
465 pub csrf_token: CsrfTokenOption,
466 pub session_user: Option<TemplateSessionUser>,
467 pub mnw_base_url: std::sync::Arc<str>,
468 pub communities: Vec<AdminCommunityViewRow>,
469 pub users: Vec<AdminUserViewRow>,
470 pub search_query: String,
471 }
472