summaryrefslogtreecommitdiffstats
path: root/src/web/web.zig
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/web.zig
downloadteamdraft-6bd24af2ffbea91db1b10a5d5258980ce2068c7f.tar.gz
teamdraft-6bd24af2ffbea91db1b10a5d5258980ce2068c7f.tar.bz2
let's goHEADmain
Diffstat (limited to 'src/web/web.zig')
-rw-r--r--src/web/web.zig241
1 files changed, 241 insertions, 0 deletions
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;