package main import ( "database/sql" "errors" "fmt" "math/rand" "os" "os/user" "strings" "time" "git.tilde.town/tildetown/town/models" //"git.tilde.town/tildetown/town/review" "git.tilde.town/tildetown/town/signup" tuser "git.tilde.town/tildetown/town/user" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) func getTitle() string { titles := []string{ "yo bum rush the show", "can i kick it?", "super nintendo sega genesis", "birthdays was the worst days", "where were you when we were getting high?", "it's real time, real time, time to get real", } return titles[rand.Intn(len(titles))] } // TODO affordance for cleaning up email response type reviewer struct { db *sql.DB adminName string } func newReviewer(db *sql.DB, adminName string) *reviewer { return &reviewer{db: db, adminName: adminName} } func (r *reviewer) Review(s *models.TownSignup, decision models.SignupDecision) error { s.DecisionTime = time.Now() s.Decision = decision s.DecidedBy = r.adminName s.Insert(r.db) return nil } func (r *reviewer) AddNote(s *models.TownSignup, content string) error { note := &models.SignupNote{ Created: time.Now(), Author: r.adminName, Content: content, } note.Insert(r.db) // TODO return nil } func renderSignup(s models.TownSignup) string { out := fmt.Sprintf("[-:-:b]submitted:[-:-:-] %s\n", s.Created.Format("2006-01-02 15:04")) pairs := map[string]string{ "e-mail": s.Email, "how found / referral": s.How, "why like town / what do": s.Why, "links": s.Links, } for k, v := range pairs { out += fmt.Sprintf("[-:-:b]%s[-:-:-]\n", k) out += strings.TrimSpace(v) out += "\n\n" } return out } func _main() error { /* TODO will use this for invites userDB, err := review.ConnectDB() if err != nil { return fmt.Errorf("could not connect to user database: %w", err) } */ signupDB, err := signup.ConnectDB() if err != nil { return fmt.Errorf("could not connect to signups database: %w", err) } u, err := user.Current() if err != nil { return fmt.Errorf("that's my purse. I don't know you! %w", err) } isAdmin, err := tuser.IsAdmin(u) if err != nil { return fmt.Errorf("that's my purse. I don't know you! %w", err) } if !isAdmin { return errors.New("this command can only be run by a town admin") } r := newReviewer(signupDB, u.Username) rand.Seed(time.Now().Unix()) su := models.TownSignup{} signups, err := su.All(signupDB) if err != nil { return fmt.Errorf("could not fetch signups: %w", err) } signupIx := 0 title := tview.NewTextView() title.SetText(getTitle()) title.SetTextAlign(tview.AlignCenter) title.SetTextColor(tcell.ColorPurple) title.SetBackgroundColor(tcell.ColorBlack) appView := tview.NewTextView() appView.SetDynamicColors(true) if len(signups) == 0 { appView.SetText("no signups found.") } else { appView.SetText(renderSignup(*signups[signupIx])) } legend := tview.NewTextView() legend.SetText("s: skip r: random A: approve R: reject N: notate Q: quit") legend.SetTextColor(tcell.ColorPurple) legend.SetTextAlign(tview.AlignCenter) legend.SetBackgroundColor(tcell.ColorBlack) count := tview.NewTextView() count.SetDynamicColors(true) updateCount := func() { count.SetText(fmt.Sprintf("[-:-:b]%d pending signups[-:-:-]", len(signups))) } updateCount() bottomFlex := tview.NewFlex() bottomFlex.SetDirection(tview.FlexColumn) bottomFlex.AddItem(count, 0, 1, false) bottomFlex.AddItem(legend, 0, 10, false) mainFlex := tview.NewFlex() mainFlex.SetDirection(tview.FlexRow) mainFlex.AddItem(title, 1, -1, false) mainFlex.AddItem(appView, 0, 1, true) mainFlex.AddItem(bottomFlex, 1, -1, false) pages := tview.NewPages() errorModal := tview.NewModal() errorModal.AddButtons([]string{"damn"}) errorModal.SetDoneFunc(func(ix int, _ string) { pages.SwitchToPage("main") }) notate := tview.NewForm() notate.AddTextArea("note", "", 80, 10, 1000, func(string) {}) notate.AddButton("submit", func() { fi := notate.GetFormItemByLabel("note").(*tview.TextArea) err = r.AddNote(signups[signupIx], fi.GetText()) if err != nil { errorModal.SetText(fmt.Sprintf("error! failed to add note: %s", err.Error())) pages.SwitchToPage("error") return } // TODO force redraw of current item pages.SwitchToPage("main") }) notate.AddButton("cancel", func() { pages.SwitchToPage("main") }) pages.AddPage("main", mainFlex, true, true) pages.AddPage("error", errorModal, false, false) pages.AddPage("notate", notate, true, false) app := tview.NewApplication() app.SetRoot(pages, true) // TODO replace imperative shit with a signupManager advanceSignup := func() { if len(signups) == 0 { appView.SetText("no signups found.") return } signupIx++ if signupIx == len(signups) { signupIx = 0 } appView.SetText(renderSignup(*signups[signupIx])) } removeSignup := func(signup *models.TownSignup) { newSignups := []*models.TownSignup{} for ix, s := range signups { if ix != signupIx { newSignups = append(newSignups, s) } } signups = newSignups if len(signups) > 0 { if signupIx >= len(signups) { signupIx = 0 } } appView.SetText(renderSignup(*signups[signupIx])) } app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Rune() { case 's': advanceSignup() case 'r': if len(signups) > 0 { signupIx = rand.Intn(len(signups)) appView.SetText(renderSignup(*signups[signupIx])) } case 'A': if len(signups) == 0 { return nil } // TODO modal for collecting clean email signup := signups[signupIx] err := r.Review(signup, models.SignupAccepted) if err != nil { errorModal.SetText(fmt.Sprintf("error! failed to approve '%d': %s", signup.ID, err.Error())) pages.SwitchToPage("error") return nil } removeSignup(signup) updateCount() // TODO generate invite token // TODO send invite email case 'R': if len(signups) == 0 { return nil } signup := signups[signupIx] err = r.Review(signup, models.SignupRejected) if err != nil { errorModal.SetText(fmt.Sprintf("error! failed to reject '%d': %s", signup.ID, err.Error())) pages.SwitchToPage("error") return nil } removeSignup(signup) updateCount() case 'N': if len(signups) == 0 { return nil } pages.SwitchToPage("notate") return nil case 'Q': app.Stop() } return event }) return app.Run() } func main() { err := _main() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }