diff --git a/handlers/syndicate.go b/handlers/syndicate.go new file mode 100644 index 0000000..cf2075e --- /dev/null +++ b/handlers/syndicate.go @@ -0,0 +1,236 @@ +package handlers + +import ( + "database/sql" + "fmt" + "net/http" + "synlotto-website/helpers" + "synlotto-website/models" + "synlotto-website/storage" +) + +func CreateSyndicateHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + data := BuildTemplateData(db, w, r) + context := helpers.TemplateContext(w, r, data) + tmpl := helpers.LoadTemplateFiles("create-syndicate.html", "templates/account/syndicates/create.html") + tmpl.ExecuteTemplate(w, "layout", context) + + case http.MethodPost: + name := r.FormValue("name") + description := r.FormValue("description") + + userID, ok := helpers.GetCurrentUserID(r) + if !ok || name == "" { + helpers.SetFlash(w, r, "Invalid data submitted") + http.Redirect(w, r, "/account/syndicates/create", http.StatusSeeOther) + return + } + + err := storage.CreateSyndicate(db, name, description, userID) + if err != nil { + helpers.SetFlash(w, r, "Failed to create syndicate") + } else { + helpers.SetFlash(w, r, "Syndicate created successfully") + } + + http.Redirect(w, r, "/account/syndicates", http.StatusSeeOther) + default: + helpers.RenderError(w, r, http.StatusMethodNotAllowed) + } + } +} + +func ListSyndicatesHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + userID, ok := helpers.GetCurrentUserID(r) + if !ok { + helpers.RenderError(w, r, 403) // ToDo need to make this use the handler so i dont need to define errors. + return + } + + managed := storage.GetSyndicatesByOwner(db, userID) + member := storage.GetSyndicatesByMember(db, userID) + + data := BuildTemplateData(db, w, r) + context := helpers.TemplateContext(w, r, data) + context["ManagedSyndicates"] = managed + context["JoinedSyndicates"] = member + + tmpl := helpers.LoadTemplateFiles("syndicates.html", "templates/account/syndicates/index.html") + tmpl.ExecuteTemplate(w, "layout", context) + } +} + +func ViewSyndicateHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + userID, ok := helpers.GetCurrentUserID(r) + if !ok { + helpers.RenderError(w, r, 403) + return + } + + syndicateID := helpers.Atoi(r.URL.Query().Get("id")) + syndicate, err := storage.GetSyndicateByID(db, syndicateID) + if err != nil || syndicate == nil { + helpers.RenderError(w, r, 404) + return + } + + isManager := userID == syndicate.OwnerID + isMember := storage.IsSyndicateMember(db, syndicateID, userID) + + if !isManager && !isMember { + helpers.RenderError(w, r, 403) + return + } + + members := storage.GetSyndicateMembers(db, syndicateID) + + data := BuildTemplateData(db, w, r) + context := helpers.TemplateContext(w, r, data) + context["Syndicate"] = syndicate + context["Members"] = members + context["IsManager"] = isManager + + tmpl := helpers.LoadTemplateFiles("syndicate-view.html", "templates/syndicates/view.html") + tmpl.ExecuteTemplate(w, "layout", context) + } +} + +func InviteMemberHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + userID, ok := helpers.GetCurrentUserID(r) + if !ok { + helpers.RenderError(w, r, 403) + return + } + + syndicateID := helpers.Atoi(r.URL.Query().Get("id")) + syndicate, err := storage.GetSyndicateByID(db, syndicateID) + if err != nil || syndicate == nil || syndicate.OwnerID != userID { + helpers.RenderError(w, r, 403) + return + } + + switch r.Method { + case http.MethodGet: + data := BuildTemplateData(db, w, r) + context := helpers.TemplateContext(w, r, data) + context["Syndicate"] = syndicate + + tmpl := helpers.LoadTemplateFiles("invite.html", "templates/syndicates/invite.html") + tmpl.ExecuteTemplate(w, "layout", context) + + case http.MethodPost: + username := r.FormValue("username") + invitee := storage.GetUserByUsername(db, username) + + if invitee == nil { + helpers.SetFlash(w, r, "User not found.") + } else if storage.IsSyndicateMember(db, syndicateID, invitee.Id) { + helpers.SetFlash(w, r, "User is already a member.") + } else { + err := storage.AddMemberToSyndicate(db, syndicateID, invitee.Id) + if err != nil { + helpers.SetFlash(w, r, "Failed to invite user.") + } else { + helpers.SetFlash(w, r, "User successfully invited.") + } + } + + http.Redirect(w, r, fmt.Sprintf("/account/syndicates/view?id=%d", syndicateID), http.StatusSeeOther) + + default: + helpers.RenderError(w, r, 405) + } + } +} + +func SyndicateLogTicketHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + userID, ok := helpers.GetCurrentUserID(r) + if !ok { + helpers.RenderError(w, r, 403) + return + } + + syndicateId := helpers.Atoi(r.URL.Query().Get("id")) + syndicate, err := storage.GetSyndicateByID(db, syndicateId) + if err != nil || syndicate.OwnerID != userID { + helpers.RenderError(w, r, 403) + return + } + + switch r.Method { + case http.MethodGet: + data := BuildTemplateData(db, w, r) + context := helpers.TemplateContext(w, r, data) + context["Syndicate"] = syndicate + + tmpl := helpers.LoadTemplateFiles("syndicate-log-ticket.html", "templates/syndicates/log_ticket.html") + tmpl.ExecuteTemplate(w, "layout", context) + + case http.MethodPost: + gameType := r.FormValue("game_type") + drawDate := r.FormValue("draw_date") + method := r.FormValue("purchase_method") + + balls := helpers.ParseBallFields(r, gameType) + + err := storage.InsertTicket(db, models.Ticket{ + UserId: userID, + GameType: gameType, + DrawDate: drawDate, + PurchaseMethod: method, + SyndicateId: &syndicateId, + // ToDo image path + }) + + if err != nil { + helpers.SetFlash(w, r, "Failed to add ticket.") + } else { + helpers.SetFlash(w, r, "Ticket added for syndicate.") + } + + http.Redirect(w, r, fmt.Sprintf("/account/syndicates/view?id=%d", syndicateId), http.StatusSeeOther) + + default: + helpers.RenderError(w, r, 405) + } + } +} + +func SyndicateTicketsHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + userID, ok := helpers.GetCurrentUserID(r) + if !ok { + helpers.RenderError(w, r, 403) + return + } + + syndicateID := helpers.Atoi(r.URL.Query().Get("id")) + if syndicateID == 0 { + helpers.RenderError(w, r, 400) + return + } + + // Check membership + if !storage.IsSyndicateMember(db, syndicateID, userID) { + helpers.RenderError(w, r, 403) + return + } + + tickets := storage.GetSyndicateTickets(db, syndicateID) + + data := BuildTemplateData(db, w, r) + context := helpers.TemplateContext(w, r, data) + context["SyndicateID"] = syndicateID + context["Tickets"] = tickets + + tmpl := helpers.LoadTemplateFiles("syndicate-tickets.html", "templates/syndicates/tickets.html") + tmpl.ExecuteTemplate(w, "layout", context) + } +} diff --git a/handlers/syndicate_invites.go b/handlers/syndicate_invites.go new file mode 100644 index 0000000..8925355 --- /dev/null +++ b/handlers/syndicate_invites.go @@ -0,0 +1,80 @@ +package handlers + +import ( + "database/sql" + "net/http" + "synlotto-website/helpers" + "synlotto-website/models" + "synlotto-website/storage" +) + +func SyndicateInviteHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + syndicateID := helpers.Atoi(r.FormValue("syndicate_id")) + recipientUsername := r.FormValue("username") + senderID, ok := helpers.GetCurrentUserID(r) + if !ok { + helpers.RenderError(w, r, 403) + return + } + + recipient := models.GetUserByUsername(recipientUsername) + if recipient == nil { + helpers.SetFlash(w, r, "User not found") + http.Redirect(w, r, "/account/syndicates", http.StatusSeeOther) + return + } + + err := storage.InviteUserToSyndicate(db, syndicateID, recipient.Id, senderID) + if err != nil { + helpers.SetFlash(w, r, "Failed to invite user") + } else { + helpers.SetFlash(w, r, "Invitation sent to "+recipientUsername) + } + http.Redirect(w, r, "/account/syndicates", http.StatusSeeOther) + } +} + +func ViewInvitesHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + userID, ok := helpers.GetCurrentUserID(r) + if !ok { + helpers.RenderError(w, r, 403) + return + } + + invites := storage.GetPendingInvites(db, userID) + data := BuildTemplateData(db, w, r) + context := helpers.TemplateContext(w, r, data) + context["Invites"] = invites + + tmpl := helpers.LoadTemplateFiles("invites.html", "templates/syndicates/invites.html") + tmpl.ExecuteTemplate(w, "layout", context) + } +} + +func AcceptInviteHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + inviteID := helpers.Atoi(r.URL.Query().Get("id")) + userID, ok := helpers.GetCurrentUserID(r) + if !ok { + helpers.RenderError(w, r, 403) + return + } + err := storage.AcceptInvite(db, inviteID, userID) + if err != nil { + helpers.SetFlash(w, r, "Failed to accept invite") + } else { + helpers.SetFlash(w, r, "You have joined the syndicate") + } + http.Redirect(w, r, "/account/syndicates", http.StatusSeeOther) + } +} + +func DeclineInviteHandler(db *sql.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + inviteID := helpers.Atoi(r.URL.Query().Get("id")) + _ = storage.UpdateInviteStatus(db, inviteID, "declined") + http.Redirect(w, r, "/account/syndicates/invites", http.StatusSeeOther) + } +} diff --git a/main.go b/main.go index c5edc5e..bb516a6 100644 --- a/main.go +++ b/main.go @@ -30,6 +30,7 @@ func main() { setupAdminRoutes(mux, db) setupAccountRoutes(mux, db) setupResultRoutes(mux, db) + setupSyndicateRoutes(mux, db) mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static")))) @@ -81,3 +82,16 @@ func setupAccountRoutes(mux *http.ServeMux, db *sql.DB) { func setupResultRoutes(mux *http.ServeMux, db *sql.DB) { mux.HandleFunc("/results/thunderball", handlers.ResultsThunderball(db)) } + +func setupSyndicateRoutes(mux *http.ServeMux, db *sql.DB) { + mux.HandleFunc("/account/syndicates", middleware.Auth(true)(handlers.ListSyndicatesHandler(db))) + mux.HandleFunc("/account/syndicates/invite", middleware.Auth(true)(handlers.InviteMemberHandler(db))) + mux.HandleFunc("/account/syndicates/view", middleware.Auth(true)(handlers.ViewSyndicateHandler(db))) + mux.HandleFunc("/account/syndicates/tickets", middleware.Auth(true)(handlers.SyndicateTicketsHandler(db))) + mux.HandleFunc("/account/syndicates/tickets/new", middleware.Auth(true)(handlers.SyndicateLogTicketHandler(db))) + mux.HandleFunc("/account/syndicates/invite", middleware.Auth(true)(handlers.SyndicateInviteHandler(db))) + mux.HandleFunc("/account/syndicates/invites", middleware.Auth(true)(handlers.ViewInvitesHandler(db))) + mux.HandleFunc("/account/syndicates/invites/accept", middleware.Auth(true)(handlers.AcceptInviteHandler(db))) + mux.HandleFunc("/account/syndicates/invites/decline", middleware.Auth(true)(handlers.DeclineInviteHandler(db))) + +} diff --git a/models/syndicate.go b/models/syndicate.go new file mode 100644 index 0000000..8ed274d --- /dev/null +++ b/models/syndicate.go @@ -0,0 +1,29 @@ +package models + +import "time" + +type Syndicate struct { + ID int + OwnerID int + Name string + Description string + CreatedBy int + CreatedAt time.Time +} + +type SyndicateMember struct { + ID int + SyndicateID int + UserID int + Role string + JoinedAt time.Time +} + +type SyndicateInvite struct { + ID int + SyndicateID int + InvitedUserID int + SentByUserID int + Status string + CreatedAt time.Time +} diff --git a/models/ticket.go b/models/ticket.go index 0a4038f..13c3be0 100644 --- a/models/ticket.go +++ b/models/ticket.go @@ -2,6 +2,8 @@ package models type Ticket struct { Id int + UserId int + SyndicateId *int GameType string DrawDate string Ball1 int diff --git a/storage/db.go b/storage/db.go index 1fe87ec..9541c43 100644 --- a/storage/db.go +++ b/storage/db.go @@ -25,6 +25,9 @@ func InitDB(filepath string) *sql.DB { SchemaLogTicketMatching, SchemaAdminAccessLog, SchemaNewAuditLog, + SchemaSyndicates, + SchemaSyndicateMembers, + SchemaSyndicateInvites, } for _, stmt := range schemas { diff --git a/storage/schema.go b/storage/schema.go index a5177c0..ef66509 100644 --- a/storage/schema.go +++ b/storage/schema.go @@ -108,6 +108,7 @@ CREATE TABLE IF NOT EXISTS my_tickets ( is_winner BOOLEAN, prize_amount INTEGER, prize_label TEXT, + syndicate_id INTEGER, FOREIGN KEY (userId) REFERENCES users(id) );` @@ -173,3 +174,38 @@ CREATE TABLE IF NOT EXISTS audit_log ( user_agent TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP );` + +const SchemaSyndicates = ` +CREATE TABLE IF NOT EXISTS syndicates ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + owner_id INTEGER NOT NULL, + join_code TEXT UNIQUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (owner_id) REFERENCES users(id) +);` + +const SchemaSyndicateMembers = ` +CREATE TABLE IF NOT EXISTS syndicate_members ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + syndicate_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + role TEXT DEFAULT 'member', -- owner, manager, member + status TEXT DEFAULT 'active', -- pending, accepted, kicked + joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (syndicate_id) REFERENCES syndicates(id), + FOREIGN KEY (user_id) REFERENCES users(id) +);` + +const SchemaSyndicateInvites = ` +CREATE TABLE IF NOT EXISTS syndicate_invites ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + syndicate_id INTEGER NOT NULL, + invited_user_id INTEGER NOT NULL, + sent_by_user_id INTEGER NOT NULL, + status TEXT DEFAULT 'pending', -- 'pending', 'accepted', 'declined' + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(syndicate_id) REFERENCES syndicates(id), + FOREIGN KEY(invited_user_id) REFERENCES users(id) +);` diff --git a/storage/syndicate.go b/storage/syndicate.go new file mode 100644 index 0000000..acae356 --- /dev/null +++ b/storage/syndicate.go @@ -0,0 +1,193 @@ +package storage + +import ( + "database/sql" + "synlotto-website/models" +) + +func GetSyndicatesByOwner(db *sql.DB, ownerID int) []models.Syndicate { + rows, err := db.Query(` + SELECT id, name, description, created_at, owner_id + FROM syndicates + WHERE owner_id = ?`, ownerID) + if err != nil { + return nil + } + defer rows.Close() + + var syndicates []models.Syndicate + for rows.Next() { + var s models.Syndicate + err := rows.Scan(&s.ID, &s.Name, &s.Description, &s.CreatedAt, &s.OwnerID) + if err == nil { + syndicates = append(syndicates, s) + } + } + return syndicates +} + +func GetSyndicatesByMember(db *sql.DB, userID int) []models.Syndicate { + rows, err := db.Query(` + SELECT s.id, s.name, s.description, s.created_at, s.owner_id + FROM syndicates s + JOIN syndicate_members m ON s.id = m.syndicate_id + WHERE m.user_id = ?`, userID) + if err != nil { + return nil + } + defer rows.Close() + + var syndicates []models.Syndicate + for rows.Next() { + var s models.Syndicate + err := rows.Scan(&s.ID, &s.Name, &s.Description, &s.CreatedAt, &s.OwnerID) + if err == nil { + syndicates = append(syndicates, s) + } + } + return syndicates +} + +func GetSyndicateByID(db *sql.DB, id int) (*models.Syndicate, error) { + row := db.QueryRow(`SELECT id, name, description, owner_id, created_at FROM syndicates WHERE id = ?`, id) + var s models.Syndicate + err := row.Scan(&s.ID, &s.Name, &s.Description, &s.OwnerID, &s.CreatedAt) + if err != nil { + return nil, err + } + return &s, nil +} + +func GetSyndicateMembers(db *sql.DB, syndicateID int) []models.SyndicateMember { + rows, err := db.Query(` + SELECT m.user_id, u.username, m.joined_at + FROM syndicate_members m + JOIN users u ON u.id = m.user_id + WHERE m.syndicate_id = ? + `, syndicateID) + if err != nil { + return nil + } + defer rows.Close() + + var members []models.SyndicateMember + for rows.Next() { + var m models.SyndicateMember + err := rows.Scan(&m.UserID, &m.UserID, &m.JoinedAt) + if err == nil { + members = append(members, m) + } + } + return members +} + +func IsSyndicateMember(db *sql.DB, syndicateID, userID int) bool { + var count int + err := db.QueryRow(`SELECT COUNT(*) FROM syndicate_members WHERE syndicate_id = ? AND user_id = ?`, syndicateID, userID).Scan(&count) + return err == nil && count > 0 +} + +func GetUserByUsername(db *sql.DB, username string) *models.User { + row := db.QueryRow(`SELECT id, username, is_admin FROM users WHERE username = ?`, username) + var u models.User + err := row.Scan(&u.Id, &u.Username, &u.IsAdmin) + if err != nil { + return nil + } + return &u +} + +func AddMemberToSyndicate(db *sql.DB, syndicateID, userID int) error { + _, err := db.Exec(` + INSERT INTO syndicate_members (syndicate_id, user_id, joined_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + `, syndicateID, userID) + return err +} + +func GetSyndicateTickets(db *sql.DB, syndicateID int) []models.Ticket { + rows, err := db.Query(` + SELECT id, userId, syndicateId, game_type, draw_date, ball1, ball2, ball3, ball4, ball5, ball6, + bonus1, bonus2, matched_main, matched_bonus, prize_tier, prize_amount, prize_label, is_winner + FROM my_tickets + WHERE syndicateId = ? + ORDER BY draw_date DESC + `, syndicateID) + if err != nil { + return nil + } + defer rows.Close() + + var tickets []models.Ticket + for rows.Next() { + var t models.Ticket + err := rows.Scan( + &t.Id, &t.UserId, &t.SyndicateId, &t.GameType, &t.DrawDate, + &t.Ball1, &t.Ball2, &t.Ball3, &t.Ball4, &t.Ball5, &t.Ball6, + &t.Bonus1, &t.Bonus2, &t.MatchedMain, &t.MatchedBonus, + &t.PrizeTier, &t.PrizeAmount, &t.PrizeLabel, &t.IsWinner, + ) + if err == nil { + tickets = append(tickets, t) + } + } + return tickets +} + +func InviteUserToSyndicate(db *sql.DB, syndicateID, invitedUserID, senderID int) error { + _, err := db.Exec(` + INSERT INTO syndicate_invites (syndicate_id, invited_user_id, sent_by_user_id) + VALUES (?, ?, ?) + `, syndicateID, invitedUserID, senderID) + return err +} + +func GetPendingInvites(db *sql.DB, userID int) []models.SyndicateInvite { + rows, err := db.Query(` + SELECT id, syndicate_id, invited_user_id, sent_by_user_id, status, created_at + FROM syndicate_invites + WHERE invited_user_id = ? AND status = 'pending' + `, userID) + if err != nil { + return nil + } + defer rows.Close() + + var invites []models.SyndicateInvite + for rows.Next() { + var i models.SyndicateInvite + rows.Scan(&i.ID, &i.SyndicateID, &i.InvitedUserID, &i.SentByUserID, &i.Status, &i.CreatedAt) + invites = append(invites, i) + } + return invites +} + +func UpdateInviteStatus(db *sql.DB, inviteID int, status string) error { + _, err := db.Exec(` + UPDATE syndicate_invites + SET status = ? + WHERE id = ? + `, status, inviteID) + return err +} + +func AcceptInvite(db *sql.DB, inviteID, userID int) error { + var syndicateID int + err := db.QueryRow(` + SELECT syndicate_id FROM syndicate_invites + WHERE id = ? AND invited_user_id = ? AND status = 'pending' + `, inviteID, userID).Scan(&syndicateID) + if err != nil { + return err + } + + if err := UpdateInviteStatus(db, inviteID, "accepted"); err != nil { + return err + } + + _, err = db.Exec(` + INSERT INTO syndicate_members (syndicate_id, user_id, joined_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + `, syndicateID, userID) + return err +} diff --git a/templates/syndicates/index.html b/templates/syndicates/index.html new file mode 100644 index 0000000..c55b1ae --- /dev/null +++ b/templates/syndicates/index.html @@ -0,0 +1,41 @@ +{{ define "content" }} +
+

Your Syndicates

+ + {{ if .ManagedSyndicates }} +

Managed

+ + {{ end }} + + {{ if .JoinedSyndicates }} +

Member

+ + {{ end }} + + {{ if not .ManagedSyndicates | and (not .JoinedSyndicates) }} +
You are not part of any syndicates yet.
+ {{ end }} + + Create New Syndicate +
+{{ end }} diff --git a/templates/syndicates/invite.html b/templates/syndicates/invite.html new file mode 100644 index 0000000..5adb463 --- /dev/null +++ b/templates/syndicates/invite.html @@ -0,0 +1,18 @@ +{{ define "content" }} +
+

Invite Member to "{{ .Syndicate.Name }}"

+ {{ if .Flash }} +
{{ .Flash }}
+ {{ end }} + +
+ {{ .CSRFField }} +
+ + +
+ + Cancel +
+
+{{ end }} diff --git a/templates/syndicates/log_ticket.html b/templates/syndicates/log_ticket.html new file mode 100644 index 0000000..957ff73 --- /dev/null +++ b/templates/syndicates/log_ticket.html @@ -0,0 +1,34 @@ +{{ define "content" }} +
+

Add Ticket for {{ .Syndicate.Name }}

+ +
+ {{ .CSRFField }} + +
+ + +
+ +
+ + +
+ +
+ + +
+ + + {{ template "ballInputs" . }} + + + Cancel +
+
+{{ end }} diff --git a/templates/syndicates/tickets.html b/templates/syndicates/tickets.html new file mode 100644 index 0000000..f5ab0f9 --- /dev/null +++ b/templates/syndicates/tickets.html @@ -0,0 +1,42 @@ +{{ define "content" }} +
+

Syndicate Tickets

+ + {{ if .Tickets }} + + + + + + + + + + + + {{ range .Tickets }} + + + + + + + + {{ end }} + +
GameDraw DateNumbersMatchedPrize
{{ .GameType }}{{ .DrawDate }} + {{ .Ball1 }} {{ .Ball2 }} {{ .Ball3 }} {{ .Ball4 }} {{ .Ball5 }} {{ .Ball6 }} + {{ if .Bonus1 }}+{{ .Bonus1 }}{{ end }} + {{ if .Bonus2 }} {{ .Bonus2 }}{{ end }} + {{ .MatchedMain }} + {{ .MatchedBonus }} + {{ if .IsWinner }} + 💷 {{ .PrizeLabel }} + {{ else }} + + {{ end }} +
+ {{ else }} +
No tickets found for this syndicate.
+ {{ end }} +
+{{ end }} diff --git a/templates/syndicates/view.html b/templates/syndicates/view.html new file mode 100644 index 0000000..7fa268e --- /dev/null +++ b/templates/syndicates/view.html @@ -0,0 +1,28 @@ +{{ define "content" }} +
+

{{ .Syndicate.Name }}

+

{{ .Syndicate.Description }}

+ +
+ +

Members

+ + + {{ if .IsManager }} +
+ Manager Controls
+ You can add or remove members, and manage tickets. +
+ Invite Members + {{ end }} + + ← Back to Syndicates +
+{{ end }}