+// html {{{
+fn html_form(res: *http.Response, comptime fmt_action: []const u8, args_action: anytype, inputs: anytype) !void {
+ try res.write("<form action=\"", .{});
+ try res.write(fmt_action, args_action);
+ try res.write("\" method=\"post\">", .{});
+
+ inline for (inputs) |input| {
+ switch (@typeInfo(@TypeOf(input))) {
+ .Struct => {
+ try res.write("<input ", .{});
+ try res.write(input[0], input[1]);
+ try res.write(" />", .{});
+ },
+ else => {
+ try res.write("<input ", .{});
+ try res.write(input, .{});
+ try res.write(" />", .{});
+ },
+ }
+ }
+
+ try res.write("</form>", .{});
+}
+// }}}
+
+// write {{{
+const TimeStr = std.BoundedArray(u8, 256);
+
+// http://howardhinnant.github.io/date_algorithms.html
+fn time_str(_t: i64) TimeStr {
+ const t: u64 = @intCast(_t);
+ var result = TimeStr.init(0) catch unreachable;
+
+ const nD = @divFloor(t, std.time.s_per_day);
+ const z: u64 = nD + 719468;
+ const era: u64 = (if (z >= 0) z else z - 146096) / 146097;
+ const doe: u64 = z - era * 146097; // [0, 146096]
+ const yoe: u64 = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
+ const Y: u64 = yoe + era * 400;
+ const doy: u64 = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
+ const mp: u64 = (5 * doy + 2) / 153; // [0, 11]
+ const D: u64 = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
+ const M: u64 = if (mp < 10) mp + 3 else mp - 9;
+
+ const h: u64 = @divFloor(t - nD * std.time.s_per_day, std.time.s_per_hour);
+ const m: u64 = @divFloor(t - nD * std.time.s_per_day - h * std.time.s_per_hour, std.time.s_per_min);
+ const s: u64 = t - nD * std.time.s_per_day - h * std.time.s_per_hour - m * std.time.s_per_min;
+
+ std.fmt.format(result.writer(), "<time><span>{:0>4}-{:0>2}-{:0>2} {:0>2}:{:0>2}:{:0>2} UTC</span><script>document.currentScript.parentElement.innerHTML=new Date({}).toLocaleString()</script></time>", .{ Y, M, D, h, m, s, t * 1000 }) catch unreachable;
+
+ return result;
+}
+fn write_header(res: *http.Response, logged_in: ?Login) !void {
+ if (logged_in) |login| {
+ try res.write(
+ \\<a href="/">Home</a><br />
+ , .{});
+ try res.write(
+ \\<a href="/user/{s}">Profile</a><br />
+ , .{login.user.name.constSlice()});
+ try res.write(
+ \\<a href="/post">Post</a><br />
+ , .{});
+ try html_form(res, "/logout", .{}, .{
+ \\type="submit" value="Logout"
+ });
+ try res.write("<br /><br />", .{});
+ } else {
+ try res.write(
+ \\<a href="/">Home</a><br />
+ \\<form action="/register" method="post">
+ \\<input type="text" name="username" />
+ \\<input type="password" name="password" />
+ \\<input type="submit" value="Register" />
+ \\</form>
+ \\<form action="/login" method="post">
+ \\<input type="text" name="username" />
+ \\<input type="password" name="password" />
+ \\<input type="submit" value="Login" />
+ \\</form>
+ , .{});
+ try html_form(res, "/quit", .{}, .{
+ \\type="submit" value="Quit"
+ });
+ try res.write("<br /><br />", .{});
+ }
+}
+fn write_start(res: *http.Response) !void {
+ try res.write(
+ \\<!doctype html>
+ \\<html>
+ \\<head>
+ \\<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🐣</text></svg>">
+ \\<style>
+ \\ form {
+ \\ display: inline-block;
+ \\ }
+ \\</style>
+ \\</head>
+ \\<body>
+ , .{});
+}
+fn write_end(res: *http.Response) !void {
+ try res.write("</body></html>", .{});
+}
+fn write_post(res: *http.Response, txn: lmdb.Txn, logged_in: ?Login, post_id: PostId, options: struct { recurse: u8 = 0, show_comment_field: bool = false }) !void {
+ const posts = try Db.posts(txn);
+ const post = posts.get(post_id) catch {
+ res.redirect("/") catch {};
+ return;
+ };
+ const users = try Db.users(txn);
+ const user = try users.get(post.user_id);
+
+ try res.write(
+ \\<div>
+ \\<span><a href="/user/{s}">{s}</a> {s}</span><br />
+ \\<span>{s}</span><br />
+ , .{ user.name.constSlice(), user.display_name.constSlice(), time_str(post.time).constSlice(), post.text.constSlice() });
+
+ if (post.quote_id) |quote_id| {
+ try res.write("<div style=\"border: 1px solid black;\">", .{});
+ if (options.recurse > 0) {
+ try write_post(res, txn, logged_in, quote_id, .{ .recurse = options.recurse - 1 });
+ } else {
+ try res.write("<a href=\"/post/{x}\">...</a>", .{@intFromEnum(quote_id)});
+ }
+ try res.write("</div>", .{});
+ }
+
+ const comments_view = try post.comments.open(txn);
+ const quotes_view = try post.quotes.open(txn);
+ const votes_view = try post.votes.open(txn);
+
+ // Votes
+ const vote: ?Vote = if (logged_in != null and try votes_view.has(logged_in.?.user.id)) try votes_view.get(logged_in.?.user.id) else null;
+
+ if (vote != null and vote.?.kind == .Up) {
+ try html_form(res, "/upvote", .{}, .{
+ .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
+ .{ "type=\"submit\" value=\"⬆ {}\"", .{post.upvotes} },
+ });
+ } else {
+ try html_form(res, "/upvote", .{}, .{
+ .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
+ .{ "type=\"submit\" value=\"⇧ {}\"", .{post.upvotes} },
+ });
+ }
+ if (vote != null and vote.?.kind == .Down) {
+ try html_form(res, "/downvote", .{}, .{
+ .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
+ .{ "type=\"submit\" value=\"⬇ {}\"", .{post.downvotes} },
+ });
+ } else {
+ try html_form(res, "/downvote", .{}, .{
+ .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
+ .{ "type=\"submit\" value=\"⇩ {}\"", .{post.downvotes} },
+ });
+ }
+
+ // Comment Count
+ try res.write(
+ \\<a href="/post/{x}">💭 {}</a>
+ , .{ @intFromEnum(post.id), comments_view.len() });
+
+ // Quote
+ try res.write(
+ \\<a href="/quotes/{x}">🔁 {}</a>
+ , .{ @intFromEnum(post.id), quotes_view.len() });
+
+ // Save to List
+ if (logged_in) |login| {
+ const lists_view = try login.user.post_lists.open(txn);
+ try res.write("<form action=\"/list_add\" method=\"post\">", .{});
+ try res.write("<select name=\"list_id\">", .{});
+ var it = lists_view.iterator();
+ while (it.next()) |kv| {
+ const name = kv.val.name;
+ const id = kv.val.list.idx.?;
+ try res.write("<option value=\"{x}\">{s}</option>", .{ id, name.constSlice() });
+ }
+ try res.write("</select>", .{});
+ try res.write("<input type=\"hidden\" name=\"post_id\" value=\"{x}\"></input>", .{@intFromEnum(post_id)});
+ try res.write("<input type=\"submit\" value=\"Save\"></input>", .{});
+ try res.write("</form>", .{});
+ }
+
+ // Comment field
+ // TODO: maybe always show comment field and prompt for login
+ if (options.show_comment_field and logged_in != null) {
+ try res.write("<br /><br />", .{});
+ try html_form(res, "/comment", .{}, .{
+ .{ "type=\"hidden\" value=\"{x}\" name=\"post_id\"", .{@intFromEnum(post.id)} },
+ "type=\"text\" name=\"text\" placeholder=\"Text\"",
+ "type=\"submit\" value=\"Comment\"",
+ });
+ try res.write("<br />", .{});
+ }
+
+ // Comments
+ if (options.recurse > 0 and comments_view.len() > 0) {
+ try res.write(
+ \\<details{s}>
+ \\<summary>Comments</summary>
+ , .{if (options.recurse > 1) " open" else ""});
+ try res.write("<div style=\"margin: 10px;\">", .{});
+ var it = comments_view.iterator();
+ var count: u8 = 0;
+ while (it.next()) |comment_id| {
+ try write_post(res, txn, logged_in, comment_id, .{ .recurse = options.recurse - 1 });
+ try res.write("<br />", .{});
+ if (options.recurse == 1) {
+ count += 1;
+ if (count >= 3) break;
+ }
+ }
+ try res.write(
+ \\</div>
+ \\</details>
+ , .{});
+ }