summaryrefslogtreecommitdiffstats
path: root/src/web
diff options
context:
space:
mode:
authorsadbeast <sadbeast@sadbeast.com>2024-07-16 18:16:29 -0700
committersadbeast <sadbeast@sadbeast.com>2024-10-05 16:40:55 -0700
commit6bd24af2ffbea91db1b10a5d5258980ce2068c7f (patch)
tree66634833f2d45260be5fcaf9111400eda12f03cc /src/web
downloadteamdraft-6bd24af2ffbea91db1b10a5d5258980ce2068c7f.tar.gz
teamdraft-6bd24af2ffbea91db1b10a5d5258980ce2068c7f.tar.bz2
let's goHEADmain
Diffstat (limited to 'src/web')
-rw-r--r--src/web/Cookie.zig418
-rw-r--r--src/web/handler.zig0
-rw-r--r--src/web/middleware/Logger.zig51
-rw-r--r--src/web/web.zig241
4 files changed, 710 insertions, 0 deletions
diff --git a/src/web/Cookie.zig b/src/web/Cookie.zig
new file mode 100644
index 0000000..c44bbd2
--- /dev/null
+++ b/src/web/Cookie.zig
@@ -0,0 +1,418 @@
+const std = @import("std");
+
+allocator: std.mem.Allocator,
+cookie_string: []const u8,
+cookies: std.StringArrayHashMap(*Cookie),
+modified: bool = false,
+arena: std.heap.ArenaAllocator,
+
+const Self = @This();
+
+const SameSite = enum { strict, lax, none };
+pub const CookieOptions = struct {
+ domain: []const u8 = "localhost",
+ path: []const u8 = "/",
+ same_site: ?SameSite = null,
+ secure: bool = false,
+ expires: ?i64 = null, // if used, set to time in seconds to be added to std.time.timestamp()
+ http_only: bool = false,
+ max_age: ?i64 = null,
+ partitioned: bool = false,
+};
+
+pub const Cookie = struct {
+ name: []const u8,
+ value: []const u8,
+ domain: ?[]const u8 = null,
+ path: ?[]const u8 = null,
+ same_site: ?SameSite = null,
+ secure: ?bool = null,
+ expires: ?i64 = null, // if used, set to time in seconds to be added to std.time.timestamp()
+ http_only: ?bool = null,
+ max_age: ?i64 = null,
+ partitioned: ?bool = null,
+
+ /// Build a cookie string.
+ pub fn bufPrint(self: Cookie, buf: *[4096]u8) ![]const u8 {
+ var options: CookieOptions = .{};
+ inline for (std.meta.fields(CookieOptions)) |field| {
+ @field(options, field.name) = @field(self, field.name) orelse @field(options, field.name);
+ }
+
+ // secure is required if samesite is set to none
+ const require_secure = if (options.same_site) |same_site| same_site == .none else false;
+
+ var stream = std.io.fixedBufferStream(buf);
+ const writer = stream.writer();
+
+ try writer.print("{s}={s}; path={s}; domain={s};", .{
+ self.name,
+ self.value,
+ options.path,
+ options.domain,
+ });
+
+ if (options.same_site) |same_site| try writer.print(" SameSite={s};", .{@tagName(same_site)});
+ if (options.secure or require_secure) try writer.writeAll(" Secure;");
+ if (options.expires) |expires| try writer.print(" Expires={d};", .{std.time.timestamp() + expires});
+ if (options.max_age) |max_age| try writer.print(" Max-Age={d};", .{max_age});
+ if (options.http_only) try writer.writeAll(" HttpOnly;");
+ if (options.partitioned) try writer.writeAll(" Partitioned;");
+
+ return stream.getWritten();
+ }
+
+ pub fn applyFlag(self: *Cookie, allocator: std.mem.Allocator, flag: Flag) !void {
+ switch (flag) {
+ .domain => |domain| self.domain = try allocator.dupe(u8, domain),
+ .path => |path| self.path = try allocator.dupe(u8, path),
+ .same_site => |same_site| self.same_site = same_site,
+ .secure => |secure| self.secure = secure,
+ .expires => |expires| self.expires = expires,
+ .http_only => |http_only| self.http_only = http_only,
+ .max_age => |max_age| self.max_age = max_age,
+ .partitioned => |partitioned| self.partitioned = partitioned,
+ }
+ }
+};
+
+pub fn init(allocator: std.mem.Allocator, cookie_string: []const u8) Self {
+ return .{
+ .allocator = allocator,
+ .cookie_string = cookie_string,
+ .cookies = std.StringArrayHashMap(*Cookie).init(allocator),
+ .arena = std.heap.ArenaAllocator.init(allocator),
+ };
+}
+
+pub fn deinit(self: *Self) void {
+ var it = self.cookies.iterator();
+ while (it.next()) |item| {
+ self.allocator.free(item.key_ptr.*);
+ self.allocator.free(item.value_ptr.*.value);
+ self.allocator.destroy(item.value_ptr.*);
+ }
+ self.cookies.deinit();
+ self.arena.deinit();
+}
+
+pub fn get(self: *Self, key: []const u8) ?*Cookie {
+ return self.cookies.get(key);
+}
+
+pub fn put(self: *Self, cookie: Cookie) !void {
+ self.modified = true;
+
+ if (self.cookies.fetchSwapRemove(cookie.name)) |entry| {
+ self.allocator.free(entry.key);
+ self.allocator.free(entry.value.value);
+ self.allocator.destroy(entry.value);
+ }
+ const key = try self.allocator.dupe(u8, cookie.name);
+ const ptr = try self.allocator.create(Cookie);
+ ptr.* = cookie;
+ ptr.name = key;
+ ptr.value = try self.allocator.dupe(u8, cookie.value);
+ try self.cookies.put(key, ptr);
+}
+
+pub const HeaderIterator = struct {
+ allocator: std.mem.Allocator,
+ cookies_iterator: std.StringArrayHashMap(*Cookie).Iterator,
+ buf: *[4096]u8,
+
+ pub fn init(allocator: std.mem.Allocator, cookies: *Self, buf: *[4096]u8) HeaderIterator {
+ return .{ .allocator = allocator, .cookies_iterator = cookies.cookies.iterator(), .buf = buf };
+ }
+
+ pub fn next(self: *HeaderIterator) !?[]const u8 {
+ if (self.cookies_iterator.next()) |entry| {
+ const cookie = entry.value_ptr.*;
+ return try cookie.bufPrint(self.buf);
+ } else {
+ return null;
+ }
+ }
+};
+
+pub fn headerIterator(self: *Self, buf: *[4096]u8) HeaderIterator {
+ return HeaderIterator.init(self.allocator, self, buf);
+}
+
+// https://datatracker.ietf.org/doc/html/rfc6265#section-4.2.1
+// cookie-header = "Cookie:" OWS cookie-string OWS
+// cookie-string = cookie-pair *( ";" SP cookie-pair )
+pub fn parse(self: *Self) !void {
+ var key_buf = std.ArrayList(u8).init(self.allocator);
+ var value_buf = std.ArrayList(u8).init(self.allocator);
+ var key_terminated = false;
+ var value_started = false;
+ var cookie_buf = std.ArrayList(Cookie).init(self.allocator);
+
+ defer key_buf.deinit();
+ defer value_buf.deinit();
+ defer cookie_buf.deinit();
+ defer self.modified = false;
+
+ for (self.cookie_string, 0..) |char, index| {
+ if (char == '=') {
+ key_terminated = true;
+ continue;
+ }
+
+ if (char == ';' or index == self.cookie_string.len - 1) {
+ if (char != ';') try value_buf.append(char);
+ if (parseFlag(key_buf.items, value_buf.items)) |flag| {
+ for (cookie_buf.items) |*cookie| try cookie.applyFlag(self.arena.allocator(), flag);
+ } else {
+ try cookie_buf.append(.{
+ .name = try self.arena.allocator().dupe(u8, key_buf.items),
+ .value = try self.arena.allocator().dupe(u8, value_buf.items),
+ });
+ }
+ key_buf.clearAndFree();
+ value_buf.clearAndFree();
+ value_started = false;
+ key_terminated = false;
+ continue;
+ }
+
+ if (!key_terminated and char == ' ') continue;
+
+ if (!key_terminated) {
+ try key_buf.append(char);
+ continue;
+ }
+
+ if (char == ' ' and !value_started) continue;
+ if (char != ' ' and !value_started) value_started = true;
+
+ if (key_terminated and value_started) {
+ try value_buf.append(char);
+ continue;
+ }
+
+ return error.JetzigInvalidCookieHeader;
+ }
+
+ for (cookie_buf.items) |cookie| try self.put(cookie);
+}
+
+const Flag = union(enum) {
+ domain: []const u8,
+ path: []const u8,
+ same_site: SameSite,
+ secure: bool,
+ expires: i64,
+ max_age: i64,
+ http_only: bool,
+ partitioned: bool,
+};
+
+fn parseFlag(key: []const u8, value: []const u8) ?Flag {
+ if (key.len > 64) return null;
+ if (value.len > 64) return null;
+
+ var key_buf: [64]u8 = undefined;
+
+ const normalized_key = std.ascii.lowerString(&key_buf, strip(key));
+ const normalized_value = strip(value);
+
+ if (std.mem.eql(u8, normalized_key, "domain")) {
+ return .{ .domain = normalized_value };
+ } else if (std.mem.eql(u8, normalized_key, "path")) {
+ return .{ .path = normalized_value };
+ } else if (std.mem.eql(u8, normalized_key, "samesite")) {
+ return if (std.mem.eql(u8, normalized_value, "strict"))
+ .{ .same_site = .strict }
+ else if (std.mem.eql(u8, normalized_value, "lax"))
+ .{ .same_site = .lax }
+ else
+ .{ .same_site = .none };
+ } else if (std.mem.eql(u8, normalized_key, "secure")) {
+ return .{ .secure = true };
+ } else if (std.mem.eql(u8, normalized_key, "httponly")) {
+ return .{ .http_only = true };
+ } else if (std.mem.eql(u8, normalized_key, "partitioned")) {
+ return .{ .partitioned = true };
+ } else if (std.mem.eql(u8, normalized_key, "expires")) {
+ return .{ .expires = std.fmt.parseInt(i64, normalized_value, 10) catch return null };
+ } else if (std.mem.eql(u8, normalized_key, "max-age")) {
+ return .{ .max_age = std.fmt.parseInt(i64, normalized_value, 10) catch return null };
+ } else {
+ return null;
+ }
+}
+
+inline fn strip(input: []const u8) []const u8 {
+ return std.mem.trim(u8, input, &std.ascii.whitespace);
+}
+
+test "basic cookie string" {
+ const allocator = std.testing.allocator;
+ var cookies = Self.init(allocator, "foo=bar; baz=qux;");
+ defer cookies.deinit();
+ try cookies.parse();
+ try std.testing.expectEqualStrings("bar", cookies.get("foo").?.value);
+ try std.testing.expectEqualStrings("qux", cookies.get("baz").?.value);
+}
+
+test "empty cookie string" {
+ const allocator = std.testing.allocator;
+ var cookies = Self.init(allocator, "");
+ defer cookies.deinit();
+ try cookies.parse();
+}
+
+test "cookie string with irregular spaces" {
+ const allocator = std.testing.allocator;
+ var cookies = Self.init(allocator, "foo= bar; baz= qux;");
+ defer cookies.deinit();
+ try cookies.parse();
+ try std.testing.expectEqualStrings("bar", cookies.get("foo").?.value);
+ try std.testing.expectEqualStrings("qux", cookies.get("baz").?.value);
+}
+
+test "headerIterator" {
+ const allocator = std.testing.allocator;
+ var buf = std.ArrayList(u8).init(allocator);
+ defer buf.deinit();
+
+ const writer = buf.writer();
+
+ var cookies = Self.init(allocator, "foo=bar; baz=qux;");
+ defer cookies.deinit();
+ try cookies.parse();
+
+ var it_buf: [4096]u8 = undefined;
+ var it = cookies.headerIterator(&it_buf);
+ while (try it.next()) |*header| {
+ try writer.writeAll(header.*);
+ try writer.writeAll("\n");
+ }
+
+ try std.testing.expectEqualStrings(
+ \\foo=bar; path=/; domain=localhost;
+ \\baz=qux; path=/; domain=localhost;
+ \\
+ , buf.items);
+}
+
+test "modified" {
+ const allocator = std.testing.allocator;
+ var cookies = Self.init(allocator, "foo=bar; baz=qux;");
+ defer cookies.deinit();
+
+ try cookies.parse();
+ try std.testing.expect(cookies.modified == false);
+
+ try cookies.put(.{ .name = "quux", .value = "corge" });
+ try std.testing.expect(cookies.modified == true);
+}
+
+test "domain=example.com" {
+ const allocator = std.testing.allocator;
+ var cookies = Self.init(allocator, "foo=bar; baz=qux; Domain=example.com;");
+ defer cookies.deinit();
+
+ try cookies.parse();
+ const cookie = cookies.get("foo").?;
+ try std.testing.expectEqualStrings(cookie.domain.?, "example.com");
+}
+
+test "path=/example_path" {
+ const allocator = std.testing.allocator;
+ var cookies = Self.init(allocator, "foo=bar; baz=qux; Path=/example_path;");
+ defer cookies.deinit();
+
+ try cookies.parse();
+ const cookie = cookies.get("foo").?;
+ try std.testing.expectEqualStrings(cookie.path.?, "/example_path");
+}
+
+test "SameSite=lax" {
+ const allocator = std.testing.allocator;
+ var cookies = Self.init(allocator, "foo=bar; baz=qux; SameSite=lax;");
+ defer cookies.deinit();
+
+ try cookies.parse();
+ const cookie = cookies.get("foo").?;
+ try std.testing.expect(cookie.same_site == .lax);
+}
+
+test "SameSite=none" {
+ const allocator = std.testing.allocator;
+ var cookies = Self.init(allocator, "foo=bar; baz=qux; SameSite=none;");
+ defer cookies.deinit();
+
+ try cookies.parse();
+ const cookie = cookies.get("foo").?;
+ try std.testing.expect(cookie.same_site == .none);
+}
+
+test "SameSite=strict" {
+ const allocator = std.testing.allocator;
+ var cookies = Self.init(allocator, "foo=bar; baz=qux; SameSite=strict;");
+ defer cookies.deinit();
+
+ try cookies.parse();
+ const cookie = cookies.get("foo").?;
+ try std.testing.expect(cookie.same_site == .strict);
+}
+
+test "Secure" {
+ const allocator = std.testing.allocator;
+ var cookies = Self.init(allocator, "foo=bar; baz=qux; Secure;");
+ defer cookies.deinit();
+
+ try cookies.parse();
+ const cookie = cookies.get("foo").?;
+ try std.testing.expect(cookie.secure.?);
+}
+
+test "Partitioned" {
+ const allocator = std.testing.allocator;
+ var cookies = Self.init(allocator, "foo=bar; baz=qux; Partitioned;");
+ defer cookies.deinit();
+
+ try cookies.parse();
+ const cookie = cookies.get("foo").?;
+ try std.testing.expect(cookie.partitioned.?);
+}
+
+test "Max-Age" {
+ const allocator = std.testing.allocator;
+ var cookies = Self.init(allocator, "foo=bar; baz=qux; Max-Age=123123123;");
+ defer cookies.deinit();
+
+ try cookies.parse();
+ const cookie = cookies.get("foo").?;
+ try std.testing.expect(cookie.max_age.? == 123123123);
+}
+
+test "Expires" {
+ const allocator = std.testing.allocator;
+ var cookies = Self.init(allocator, "foo=bar; baz=qux; Expires=123123123;");
+ defer cookies.deinit();
+
+ try cookies.parse();
+ const cookie = cookies.get("foo").?;
+ try std.testing.expect(cookie.expires.? == 123123123);
+}
+
+test "default flags" {
+ const allocator = std.testing.allocator;
+ var cookies = Self.init(allocator, "foo=bar; baz=qux;");
+ defer cookies.deinit();
+
+ try cookies.parse();
+ const cookie = cookies.get("foo").?;
+ try std.testing.expect(cookie.domain == null);
+ try std.testing.expect(cookie.path == null);
+ try std.testing.expect(cookie.same_site == null);
+ try std.testing.expect(cookie.secure == null);
+ try std.testing.expect(cookie.expires == null);
+ try std.testing.expect(cookie.http_only == null);
+ try std.testing.expect(cookie.max_age == null);
+ try std.testing.expect(cookie.partitioned == null);
+}
diff --git a/src/web/handler.zig b/src/web/handler.zig
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/web/handler.zig
diff --git a/src/web/middleware/Logger.zig b/src/web/middleware/Logger.zig
new file mode 100644
index 0000000..f2b1637
--- /dev/null
+++ b/src/web/middleware/Logger.zig
@@ -0,0 +1,51 @@
+const Logger = @This();
+
+query: bool,
+
+// Must define an `init` method, which will accept your Config
+// Alternatively, you can define a init(config: Config, mc: httpz.MiddlewareConfig)
+// here mc will give you access to the server's allocator and arena
+pub fn init(config: Config) !Logger {
+ return .{
+ .query = config.query,
+ };
+}
+
+// optionally you can define an "deinit" method
+// pub fn deinit(self: *Logger) void {
+
+// }
+
+// Must define an `execute` method. `self` doesn't have to be `const`, but
+// you're responsible for making your middleware thread-safe.
+pub fn execute(self: *const Logger, req: *httpz.Request, res: *httpz.Response, executor: anytype) !void {
+ const start = std.time.microTimestamp();
+
+ defer {
+ const elapsed = std.time.microTimestamp() - start;
+ var logger = logz.logger().multiuse()
+ .stringSafe("@l", "REQ")
+ .stringSafe("method", @tagName(req.method))
+ .int("status", res.status)
+ .string("path", req.url.path);
+
+ if (self.query) {
+ _ = logger.string("query", req.url.query);
+ }
+
+ logger.int("us", elapsed).log();
+ }
+
+ // If you don't call executor.next(), there will be no further processing of
+ // the request and we'll go straight to writing the response.
+ return executor.next();
+}
+
+// Must defined a pub config structure, even if it's empty
+pub const Config = struct {
+ query: bool,
+};
+
+const std = @import("std");
+const httpz = @import("httpz");
+const logz = @import("logz");
diff --git a/src/web/web.zig b/src/web/web.zig
new file mode 100644
index 0000000..e0882a0
--- /dev/null
+++ b/src/web/web.zig
@@ -0,0 +1,241 @@
+pub fn start(app: *App) !void {
+ const allocator = app.allocator;
+
+ var server = try httpz.Server(*App).init(allocator, .{
+ .address = "0.0.0.0",
+ .port = 5882,
+ .request = .{
+ .max_form_count = 4,
+ },
+ }, app);
+ defer server.deinit();
+
+ const router = server.router();
+ {
+ // publicly accessible
+ var routes = router.group("/", .{});
+
+ routes.get("/", index);
+ routes.get("/about", about);
+ routes.get("/invite/:code", acceptInvite);
+
+ routes.post("/drafts", createDraft);
+ routes.get("/drafts/:id", showDraft);
+ routes.post("/drafts/:id/pick", createPick);
+ }
+
+ const http_address = try std.fmt.allocPrint(allocator, "http://{s}:{d}", .{ "0.0.0.0", 5882 });
+ logz.info().ctx("http").string("address", http_address).log();
+ allocator.free(http_address);
+
+ // blocks
+ try server.listen();
+}
+
+fn index(_: *RequestContext, req: *httpz.Request, res: *httpz.Response) !void {
+ var data = Data.init(res.arena);
+ defer data.deinit();
+
+ const dt = try zul.DateTime.fromUnix(std.time.timestamp(), .seconds);
+ const tomorrow = try dt.add(1, .hours);
+
+ var root = try data.object();
+ try root.put("draft_time", try std.fmt.allocPrint(req.arena, "{s}T{s}", .{ tomorrow.date(), tomorrow.time() }));
+
+ try renderData("index", &data, req, res);
+}
+
+fn about(_: *RequestContext, req: *httpz.Request, res: *httpz.Response) !void {
+ try render("about", req, res);
+}
+
+fn acceptInvite(ctx: *RequestContext, req: *httpz.Request, res: *httpz.Response) !void {
+ var data = Data.init(res.arena);
+ defer data.deinit();
+
+ if (req.params.get("code")) |code| {
+ var accept_invite_result = (try ctx.app.pool.row("SELECT * FROM accept_invite($1)", .{code})) orelse {
+ res.status = 500;
+ return;
+ };
+ defer accept_invite_result.deinit() catch {};
+
+ const session_id = accept_invite_result.get([]const u8, 0);
+ const draft_id = accept_invite_result.get(i64, 1);
+ res.header("Set-Cookie", try std.fmt.allocPrint(res.arena, "s={s}; Path=/", .{session_id}));
+ res.header("Location", try std.fmt.allocPrint(res.arena, "/drafts/{}", .{draft_id}));
+
+ res.status = 302;
+ }
+}
+
+fn createPick(ctx: *RequestContext, req: *httpz.Request, res: *httpz.Response) !void {
+ const query = try req.query();
+ if (query.get("team_id")) |team_id| {
+ if (req.params.get("id")) |draft_id| {
+ _ = try ctx.app.pool.exec("CALL auto_draft($1)", .{draft_id});
+ _ = try ctx.app.pool.exec("INSERT INTO picks (draft_user_id, draft_id, team_id) VALUES (current_picker_id($1), $1, $2)", .{ draft_id, team_id });
+
+ res.header("Location", try std.fmt.allocPrint(res.arena, "/drafts/{s}", .{draft_id}));
+ res.header("HX-Location", try std.fmt.allocPrint(res.arena, "/drafts/{s}", .{draft_id}));
+
+ res.status = 302;
+ }
+ }
+}
+
+fn createDraft(ctx: *RequestContext, req: *httpz.Request, res: *httpz.Response) !void {
+ const formData = try req.formData();
+
+ if (formData.get("player1")) |player1| {
+ logz.info().string("player1", player1).log();
+ const player2 = formData.get("player2") orelse "Player 2";
+ var create_draft_result: pg.QueryRow = undefined;
+
+ if (formData.get("draft")) |_| {
+ if (formData.get("draft_time")) |draft_time| {
+ logz.info().string("draft_time", draft_time).log();
+ create_draft_result = (try ctx.app.pool.row("SELECT * FROM create_draft(1, $1, $2, $3)", .{ player1, player2, draft_time })) orelse {
+ res.status = 500;
+ return;
+ };
+ }
+ } else {
+ create_draft_result = (try ctx.app.pool.row("SELECT * FROM create_draft(1, $1, $2, null)", .{ player1, player2 })) orelse {
+ res.status = 500;
+ return;
+ };
+ }
+ defer create_draft_result.deinit() catch {};
+
+ const session_id = create_draft_result.get([]const u8, 0);
+ const draft_id = create_draft_result.get(i64, 1);
+ res.header("Set-Cookie", try std.fmt.allocPrint(res.arena, "s={s}; Path=/", .{session_id}));
+ res.header("Location", try std.fmt.allocPrint(res.arena, "/drafts/{}", .{draft_id}));
+
+ res.status = 302;
+ return;
+ }
+
+ var data = Data.init(res.arena);
+ defer data.deinit();
+ if (formData.get("draft")) |_| {
+ var root = try data.root(.object);
+ try root.put("draft", true);
+ }
+ try renderData("index", &data, req, res);
+}
+
+pub const Team = struct {
+ team_id: i32,
+ rank: ?i16,
+ name: []const u8,
+ picked: bool,
+ pick_user: ?[]const u8,
+};
+
+pub const DraftInfo = struct {
+ draft_id: i64,
+ current_player_id: ?i64,
+ current_player_name: ?[]const u8,
+ draft_status_id: i16,
+ round_time_remaining: ?i32,
+ message: ?[]const u8,
+ can_pick: bool,
+};
+
+fn showDraft(ctx: *RequestContext, req: *httpz.Request, res: *httpz.Response) !void {
+ if (req.params.get("id")) |draft_id| {
+ var draft_count = (try ctx.app.pool.row("SELECT count(1) FROM drafts WHERE draft_id = $1", .{draft_id})) orelse @panic("oh no");
+ defer draft_count.deinit() catch {};
+ if (draft_count.get(i64, 0) == 0) {
+ return ctx.app.notFound(req, res);
+ }
+
+ var data = Data.init(res.arena);
+ var root = try data.root(.object);
+
+ _ = try ctx.app.pool.exec("CALL auto_draft($1)", .{draft_id});
+
+ const picks_query = "SELECT * FROM current_draft_picks($1)";
+ var picks_result = try ctx.app.pool.query(picks_query, .{draft_id});
+ defer picks_result.deinit();
+
+ var teams = try data.array();
+ while (try picks_result.next()) |row| {
+ const team = try row.to(Team, .{});
+ var team_data = try data.object();
+ try team_data.put("name", team.name);
+ try team_data.put("pick_user", team.pick_user);
+ try team_data.put("picked", team.picked);
+ try team_data.put("rank", team.rank);
+ try team_data.put("id", team.team_id);
+ try teams.append(team_data);
+ }
+ try root.put("teams", teams);
+
+ var draft_info: DraftInfo = undefined;
+ const current_picker_query =
+ \\SELECT * FROM draft_info WHERE draft_id = $1
+ ;
+ var row = try ctx.app.pool.row(current_picker_query, .{draft_id});
+ if (row) |r| {
+ // defer r.denit();
+ draft_info = try r.to(DraftInfo, .{});
+ var can_pick = false;
+ if (draft_info.current_player_id) |current_player_id| {
+ if (ctx.user) |user| {
+ can_pick = draft_info.can_pick and current_player_id == user.id;
+ }
+ }
+ try root.put("can_pick", can_pick);
+ try root.put("running", draft_info.draft_status_id == 2);
+ try root.put("draft_id", draft_info.draft_id);
+ try root.put("message", draft_info.message);
+ try root.put("round_time_remaining", draft_info.round_time_remaining);
+ try root.put("current_picker", draft_info.current_player_name);
+ }
+
+ const code_query =
+ \\SELECT code FROM draft_user_invites
+ \\ JOIN draft_users USING(draft_user_id)
+ \\ WHERE draft_id = $1
+ ;
+ var code_result = (try ctx.app.pool.row(code_query, .{draft_id})) orelse @panic("oh no");
+ const code = code_result.get([]const u8, 0);
+ defer code_result.deinit() catch {};
+ try root.put("code", code);
+
+ defer data.deinit();
+
+ try renderData("draft", &data, req, res);
+
+ try row.?.deinit();
+ }
+}
+
+pub fn render(template_name: []const u8, req: *httpz.Request, res: *httpz.Response) !void {
+ var data = Data.init(res.arena);
+ defer data.deinit();
+ try renderData(template_name, &data, req, res);
+}
+
+pub fn renderData(template_name: []const u8, data: *Data, req: *httpz.Request, res: *httpz.Response) !void {
+ if (zmpl.find(template_name)) |template| {
+ res.body = try template.renderWithOptions(data, .{ .layout = if (req.header("hx-request")) |_| zmpl.find("body") else zmpl.find("layout") });
+ }
+}
+
+const App = @import("../App.zig");
+const RequestContext = @import("../RequestContext.zig");
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const config = @import("config");
+
+const httpz = @import("httpz");
+const logz = @import("logz");
+const pg = @import("pg");
+const zmpl = @import("zmpl");
+const zul = @import("zul");
+const Data = zmpl.Data;