From 8d018d996c1eddb882dc64ebbd228bb0135944f3 Mon Sep 17 00:00:00 2001 From: sadbeast Date: Sun, 23 Jun 2024 15:36:59 -0700 Subject: wtf --- src/web/dispatcher.zig | 52 +++++++++++++++++++++ src/web/drafts/pick.zig | 41 +++++++++++++++++ src/web/drafts/show.zig | 55 +++++++++++++++++++++++ src/web/leagues/create.zig | 78 ++++++++++++++++++++++++++++++++ src/web/leagues/invite.zig | 78 ++++++++++++++++++++++++++++++++ src/web/leagues/show.zig | 80 +++++++++++++++++++++++++++++++++ src/web/picks/pick.zig | 37 +++++++++++++++ src/web/picks/show.zig | 58 ++++++++++++++++++++++++ src/web/web.zig | 109 +++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 588 insertions(+) create mode 100644 src/web/dispatcher.zig create mode 100644 src/web/drafts/pick.zig create mode 100644 src/web/drafts/show.zig create mode 100644 src/web/leagues/create.zig create mode 100644 src/web/leagues/invite.zig create mode 100644 src/web/leagues/show.zig create mode 100644 src/web/picks/pick.zig create mode 100644 src/web/picks/show.zig create mode 100644 src/web/web.zig (limited to 'src/web') diff --git a/src/web/dispatcher.zig b/src/web/dispatcher.zig new file mode 100644 index 0000000..77ea282 --- /dev/null +++ b/src/web/dispatcher.zig @@ -0,0 +1,52 @@ +pub const Dispatcher = struct { + app: *App, + // whether to try loading the user or not, this implies requires_user = false + load_user: bool = true, + + // whether a user is required. When false, if we have a token, the user is still + // loaded (unless load_user = false). + requires_user: bool = false, + + pub fn dispatch(self: *const Dispatcher, action: httpz.Action(*Env), req: *httpz.Request, res: *httpz.Response) !void { + const app = self.app; + + var env = Env{ + .app = app, + }; + // defer env.deinit(); + // + try self.doDispatch(action, req, res, &env); + } + + fn doDispatch(_: *const Dispatcher, action: httpz.Action(*Env), req: *httpz.Request, res: *httpz.Response, env: *Env) !void { + const query = try req.query(); + const user = try loadUser(env.app, query.get("user")); + + if (user) |u| { + env.user = u; + std.debug.print("user: {}, {s}\n", .{ u.id, u.username }); + } + try action(env, req, res); + } +}; + +fn loadUser(app: *App, optional_session_id: ?[]const u8) !?User { + const session_id = optional_session_id orelse return null; + + var user: User = undefined; + const row = try app.pool.row("SELECT user_id, username FROM users WHERE user_id = $1", .{session_id}); + if (row) |r| { + user = try r.to(User, .{}); + } else { + return null; + } + + return try User.init(app.allocator, user.id, user.username); +} + +const httpz = @import("httpz"); +const std = @import("std"); + +const App = @import("../app.zig").App; +const User = @import("../User.zig"); +const Env = @import("../env.zig").Env; diff --git a/src/web/drafts/pick.zig b/src/web/drafts/pick.zig new file mode 100644 index 0000000..f710845 --- /dev/null +++ b/src/web/drafts/pick.zig @@ -0,0 +1,41 @@ +const Team = struct { + team_id: i16, + rank: ?i16, + name: []const u8, + division: []const u8, + league_user_id: ?i32, + pick_user: ?[]const u8, +}; + +pub fn handler(env: *Env, 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 env.app.pool.exec("CALL auto_draft($1)", .{draft_id}); + _ = try env.app.pool.exec("INSERT INTO picks (league_user_id, draft_id, team_id) VALUES (current_picker($1), $1, $2)", .{ draft_id, team_id }); + var result = try env.app.pool.query("SELECT cr.*, picks.league_user_id, users.name AS pick_user FROM current_rankings cr LEFT JOIN picks ON cr.team_id = picks.team_id AND picks.draft_id = $1 LEFT JOIN league_users USING(league_user_id) LEFT JOIN users USING (user_id) WHERE cr.team_id = $2", .{ draft_id, team_id }); + defer result.deinit(); + + var team: Team = undefined; + while (try result.next()) |row| { + team = try row.to(Team, .{}); + } + try web.renderWithDataPartials("pick", .{}, .{ + .team_id = team.team_id, + .rank = team.rank, + .name = team.name, + .pick_user = team.pick_user, + .division = team.division, + .league_user_id = team.league_user_id, + .draft_id = draft_id, + }, req, res); + } + } else { + @panic("oh no"); + } +} + +const httpz = @import("httpz"); +const Env = @import("../../env.zig").Env; +const web = @import("../../web/web.zig"); +const std = @import("std"); diff --git a/src/web/drafts/show.zig b/src/web/drafts/show.zig new file mode 100644 index 0000000..cd73438 --- /dev/null +++ b/src/web/drafts/show.zig @@ -0,0 +1,55 @@ +const Team = struct { + team_id: i16, + rank: ?i16, + name: []const u8, + division: []const u8, + pick_user: ?[]const u8, +}; + +const DraftInfo = struct { + draft_id: i32, + current_picker: i32, + status: []const u8, + league: []const u8, + round_time_remaining: ?i32, + message: []const u8, + can_pick: bool, +}; + +pub fn handler(env: *Env, req: *httpz.Request, res: *httpz.Response) !void { + if (req.params.get("id")) |draft_id| { + _ = try env.app.pool.exec("CALL auto_draft($1)", .{draft_id}); + + const picks_query = "SELECT * FROM current_draft_picks($1)"; + var picks_result = try env.app.pool.query(picks_query, .{draft_id}); + defer picks_result.deinit(); + + var teams: [32]Team = undefined; + var i: u8 = 0; + while (try picks_result.next()) |row| : (i += 1) { + const team = try row.to(Team, .{}); + teams[i] = team; + } + + var draft_info: DraftInfo = undefined; + const current_picker_query = + \\SELECT * FROM draft_info WHERE draft_id = $1 + ; + var row = try env.app.pool.row(current_picker_query, .{draft_id}); + if (row) |r| { + draft_info = try r.to(DraftInfo, .{}); + } + + try web.renderWithDataPartials("draft", .{ "pick", web.templates.get("pick").? }, .{ + .teams = teams, + .draft_info = draft_info, + .draft_id = draft_info.draft_id, + }, req, res); + try row.?.deinit(); + } +} + +const httpz = @import("httpz"); +const Env = @import("../../env.zig").Env; +const web = @import("../../web/web.zig"); +const std = @import("std"); diff --git a/src/web/leagues/create.zig b/src/web/leagues/create.zig new file mode 100644 index 0000000..66352cc --- /dev/null +++ b/src/web/leagues/create.zig @@ -0,0 +1,78 @@ +const League = struct { + name: []const u8, +}; + +const LeaguePickScore = struct { + league_id: i32, + team: []const u8, + player: []const u8, + win: i32, + playoffs: ?i16, + divisional: ?i16, + conference: ?i16, + superbowl: ?i16, + champion: ?i16, + total: i32, +}; + +const PlayerScore = struct { + league_id: i32, + player: []const u8, + score: i32, +}; + +pub fn handler(env: *Env, req: *httpz.Request, res: *httpz.Response) !void { + if (req.params.get("id")) |league_id| { + var league: League = undefined; + const league_query = + \\SELECT name FROM leagues WHERE league_id = $1 + ; + { + const row = try env.app.pool.row(league_query, .{league_id}); + if (row) |r| { + league = try r.to(League, .{}); + } + } + + const scores_query = + \\SELECT * FROM league_pick_scores WHERE league_id = $1 + ; + var scores_result = try env.app.pool.query(scores_query, .{league_id}); + defer scores_result.deinit(); + + var scores: [32]LeaguePickScore = undefined; + { + var i: u8 = 0; + while (try scores_result.next()) |row| : (i += 1) { + const score = try row.to(LeaguePickScore, .{}); + scores[i] = score; + } + } + + const player_scores_query = + \\SELECT * FROM player_scores WHERE league_id = $1 + ; + var player_scores_result = try env.app.pool.query(player_scores_query, .{league_id}); + defer player_scores_result.deinit(); + + var player_scores: [2]PlayerScore = undefined; + { + var i: u8 = 0; + while (try player_scores_result.next()) |row| : (i += 1) { + const score = try row.to(PlayerScore, .{}); + player_scores[i] = score; + } + } + + try web.renderWithData("league", .{ + .league = league, + .scores = scores, + .player_scores = player_scores, + }, req, res); + } +} + +const httpz = @import("httpz"); +const Env = @import("../../env.zig").Env; +const web = @import("../../web/web.zig"); +const std = @import("std"); diff --git a/src/web/leagues/invite.zig b/src/web/leagues/invite.zig new file mode 100644 index 0000000..66352cc --- /dev/null +++ b/src/web/leagues/invite.zig @@ -0,0 +1,78 @@ +const League = struct { + name: []const u8, +}; + +const LeaguePickScore = struct { + league_id: i32, + team: []const u8, + player: []const u8, + win: i32, + playoffs: ?i16, + divisional: ?i16, + conference: ?i16, + superbowl: ?i16, + champion: ?i16, + total: i32, +}; + +const PlayerScore = struct { + league_id: i32, + player: []const u8, + score: i32, +}; + +pub fn handler(env: *Env, req: *httpz.Request, res: *httpz.Response) !void { + if (req.params.get("id")) |league_id| { + var league: League = undefined; + const league_query = + \\SELECT name FROM leagues WHERE league_id = $1 + ; + { + const row = try env.app.pool.row(league_query, .{league_id}); + if (row) |r| { + league = try r.to(League, .{}); + } + } + + const scores_query = + \\SELECT * FROM league_pick_scores WHERE league_id = $1 + ; + var scores_result = try env.app.pool.query(scores_query, .{league_id}); + defer scores_result.deinit(); + + var scores: [32]LeaguePickScore = undefined; + { + var i: u8 = 0; + while (try scores_result.next()) |row| : (i += 1) { + const score = try row.to(LeaguePickScore, .{}); + scores[i] = score; + } + } + + const player_scores_query = + \\SELECT * FROM player_scores WHERE league_id = $1 + ; + var player_scores_result = try env.app.pool.query(player_scores_query, .{league_id}); + defer player_scores_result.deinit(); + + var player_scores: [2]PlayerScore = undefined; + { + var i: u8 = 0; + while (try player_scores_result.next()) |row| : (i += 1) { + const score = try row.to(PlayerScore, .{}); + player_scores[i] = score; + } + } + + try web.renderWithData("league", .{ + .league = league, + .scores = scores, + .player_scores = player_scores, + }, req, res); + } +} + +const httpz = @import("httpz"); +const Env = @import("../../env.zig").Env; +const web = @import("../../web/web.zig"); +const std = @import("std"); diff --git a/src/web/leagues/show.zig b/src/web/leagues/show.zig new file mode 100644 index 0000000..2bbdabd --- /dev/null +++ b/src/web/leagues/show.zig @@ -0,0 +1,80 @@ +const League = struct { + name: []const u8, +}; + +const LeaguePickScore = struct { + season_id: i32, + league_id: i32, + team: []const u8, + player: []const u8, + win: i32, + playoffs: ?i16, + divisional: ?i16, + conference: ?i16, + superbowl: ?i16, + champion: ?i16, + total: i32, +}; + +const PlayerScore = struct { + player: []const u8, + score: i32, +}; + +pub fn handler(env: *Env, req: *httpz.Request, res: *httpz.Response) !void { + if (req.params.get("id")) |league_id| { + const query = try req.query(); + var league: League = undefined; + const league_query = + \\SELECT name FROM leagues WHERE league_id = $1 + ; + var league_result = (try env.app.pool.row(league_query, .{league_id})) orelse { + res.status = 404; + return; + }; + defer league_result.deinit() catch {}; + + league = try league_result.to(League, .{}); + + var team_scores_result: *pg.Result = undefined; + var player_scores_result: *pg.Result = undefined; + + if (query.get("season")) |season_id| { + player_scores_result = try env.app.pool.query("SELECT player, score FROM season_player_scores WHERE league_id = $1 AND season_id = season_id_by_year($2)", .{ league_id, season_id }); + team_scores_result = try env.app.pool.query("SELECT * FROM league_scores WHERE league_id = $1 AND season_id = season_id_by_year($2)", .{ league_id, season_id }); + } else { + player_scores_result = try env.app.pool.query("SELECT player, score FROM current_player_scores WHERE league_id = $1", .{league_id}); + team_scores_result = try env.app.pool.query("SELECT * FROM league_scores WHERE league_id = $1 AND season_id = current_season()", .{league_id}); + } + defer player_scores_result.deinit(); + defer team_scores_result.deinit(); + + var scores: [32]LeaguePickScore = undefined; + var score_count: u8 = 0; + while (try team_scores_result.next()) |row| : (score_count += 1) { + const score = try row.to(LeaguePickScore, .{ .dupe = true }); + scores[score_count] = score; + } + + var player_scores: [2]PlayerScore = undefined; + { + var i: u8 = 0; + while (try player_scores_result.next()) |row| : (i += 1) { + const score = try row.to(PlayerScore, .{}); + player_scores[i] = score; + } + } + + try web.renderWithData("league", .{ + .league = league, + .scores = scores[0..score_count], + .player_scores = player_scores, + }, req, res); + } +} + +const httpz = @import("httpz"); +const pg = @import("pg"); +const Env = @import("../../env.zig").Env; +const web = @import("../../web/web.zig"); +const std = @import("std"); diff --git a/src/web/picks/pick.zig b/src/web/picks/pick.zig new file mode 100644 index 0000000..51e05c4 --- /dev/null +++ b/src/web/picks/pick.zig @@ -0,0 +1,37 @@ +const Team = struct { + team_id: i16, + rank: ?i16, + name: []const u8, + division: []const u8, + league_user_id: ?i32, +}; + +pub fn handler(env: *Env, 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| { + var result = try env.app.pool.query("SELECT cr.*, picks.league_user_id from current_rankings cr left join picks on cr.team_id = picks.team_id AND picks.draft_id = $1 WHERE cr.team_id = $2", .{ draft_id, team_id }); + defer result.deinit(); + + var team: Team = undefined; + while (try result.next()) |row| { + team = try row.to(Team, .{}); + } + try web.renderWithDataPartials("pick", .{}, .{ + .team_id = team.team_id, + .rank = team.rank, + .name = team.name, + .division = team.division, + .league_user_id = team.league_user_id, + .draft_id = draft_id, + }, req, res); + } + } else { + @panic("oh no"); + } +} + +const httpz = @import("httpz"); +const Env = @import("../../env.zig").Env; +const web = @import("../../web/web.zig"); +const std = @import("std"); diff --git a/src/web/picks/show.zig b/src/web/picks/show.zig new file mode 100644 index 0000000..f802069 --- /dev/null +++ b/src/web/picks/show.zig @@ -0,0 +1,58 @@ +const Team = struct { + team_id: i16, + rank: ?i16, + name: []const u8, + division: []const u8, + username: ?[]const u8, +}; + +const DraftInfo = struct { + draft_id: i32, + current_picker: i32, + status: []const u8, + round_time_remaining: ?i32, + message: []const u8, +}; + +pub fn handler(env: *Env, req: *httpz.Request, res: *httpz.Response) !void { + if (req.params.get("id")) |draft_id| { + _ = try env.app.pool.exec("CALL auto_draft($1)", .{draft_id}); + + const picks_query = + \\SELECT cr.*, users.username from current_rankings cr + \\LEFT JOIN picks ON cr.team_id = picks.team_id AND picks.draft_id = $1 + \\LEFT JOIN league_users ON league_users.league_user_id = picks.league_user_id + \\LEFT JOIN users ON users.user_id = league_users.user_id + ; + var picks_result = try env.app.pool.query(picks_query, .{draft_id}); + defer picks_result.deinit(); + + var teams: [32]Team = undefined; + var i: u8 = 0; + while (try picks_result.next()) |row| : (i += 1) { + const team = try row.to(Team, .{}); + teams[i] = team; + } + + var draft_info: DraftInfo = undefined; + const current_picker_query = + \\SELECT * FROM draft_info WHERE draft_id = $1 + ; + var row = try env.app.pool.row(current_picker_query, .{draft_id}); + if (row) |r| { + draft_info = try r.to(DraftInfo, .{}); + } + + try web.renderWithDataPartials("draft", .{ "pick", web.pick_template }, .{ + .teams = teams, + .draft_info = draft_info, + .can_pick = if (draft_info.round_time_remaining) |_| true else false, + }, req, res); + try row.?.deinit(); + } +} + +const httpz = @import("httpz"); +const Env = @import("../../env.zig").Env; +const web = @import("../../web/web.zig"); +const std = @import("std"); diff --git a/src/web/web.zig b/src/web/web.zig new file mode 100644 index 0000000..184a1a3 --- /dev/null +++ b/src/web/web.zig @@ -0,0 +1,109 @@ +pub const Templates = std.StringHashMap(Template); + +pub var templates: Templates = undefined; + +pub fn start(app: *App) !void { + const allocator = app.allocator; + + var server = try httpz.ServerCtx(*const Dispatcher, *Env).init(allocator, .{ + .address = "0.0.0.0", + .port = 5882, + }, undefined); + defer server.deinit(); + + templates = Templates.init(allocator); + defer { + var it = templates.valueIterator(); + while (it.next()) |t| { + t.deinit(allocator); + } + templates.deinit(); + } + + inline for (config.templates) |template| { + try templates.put(template, try loadTemplate(allocator, template)); + } + + server.dispatcher(Dispatcher.dispatch); + + var router = server.router(); + { + var routes = router.group("", .{ .ctx = &Dispatcher{ .app = app } }); + routes.get("/", index); + routes.get("/about", about); + routes.get("/drafts/:id", @import("drafts/show.zig").handler); + routes.get("/drafts/:id/pick", @import("drafts/pick.zig").handler); + routes.get("/leagues/:id", @import("leagues/show.zig").handler); + routes.post("/leagues", @import("leagues/create.zig").handler); + routes.get("/invites/:id", @import("leagues/invite.zig").handler); + } + + // use get/post/put/head/patch/options/delete + // you can also use "all" to attach to all methods + + // start the server in the current thread, blocking. + try server.listen(); +} + +fn pick(_: *Env, req: *httpz.Request, res: *httpz.Response) !void { + try render("pick", req, res); +} + +fn about(_: *Env, req: *httpz.Request, res: *httpz.Response) !void { + // try render(templates.get("about").?, req, res); + try renderWithData("about", .{ .title = "foo" }, req, res); +} + +fn index(_: *Env, req: *httpz.Request, res: *httpz.Response) !void { + try render("index", req, res); +} + +fn loadTemplate(allocator: std.mem.Allocator, comptime template_name: []const u8) !Template { + return switch (try mustache.parseText(allocator, @embedFile("../views/" ++ template_name ++ ".mustache"), .{}, .{ .copy_strings = false })) { + .success => |t| t, + .parse_error => @panic("layout parse error"), + }; +} + +fn loadTemplateFile(allocator: std.mem.Allocator, template_name: []const u8) !Template { + const file = std.fs.cwd().openFile(try std.fmt.allocPrint(allocator, "src/views/{s}.mustache", .{template_name}), .{}) catch @panic("could not find template"); + defer file.close(); + const template_bytes = file.readToEndAlloc(allocator, std.math.maxInt(usize)) catch @panic("couldn't read template file "); + + return switch (try mustache.parseText(allocator, template_bytes, .{}, .{ .copy_strings = false })) { + .success => |t| t, + .parse_error => @panic("layout parse error"), + }; +} + +pub fn renderWithDataPartials(template_name: []const u8, partials: anytype, data: anytype, req: *httpz.Request, res: *httpz.Response) !void { + var template: Template = undefined; + if (config.dev) { + template = try loadTemplateFile(res.arena, template_name); + } else { + template = templates.get(template_name).?; + } + const content = try mustache.allocRenderPartials(res.arena, template, partials, data); + if (req.header("hx-request")) |_| { + res.body = content; + } else { + res.body = try mustache.allocRender(res.arena, templates.get("layout").?, .{ .content = content }); + } +} + +pub fn renderWithData(template_name: []const u8, data: anytype, req: *httpz.Request, res: *httpz.Response) !void { + try renderWithDataPartials(template_name, .{}, data, req, res); +} + +pub fn render(template_name: []const u8, req: *httpz.Request, res: *httpz.Response) !void { + try renderWithData(template_name, .{}, req, res); +} + +const config = @import("config"); +const App = @import("../app.zig").App; +const Env = @import("../env.zig").Env; +const httpz = @import("httpz"); +const mustache = @import("mustache"); +const Template = mustache.Template; +const std = @import("std"); +const Dispatcher = @import("dispatcher.zig").Dispatcher; -- cgit v1.2.3