render.gno
5.24 Kb · 148 lines
1package clubpoll
2
3import (
4 "strconv"
5 "strings"
6 "time"
7
8 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/core/v1"
9 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/render/v1"
10)
11
12// Render serves the native pages: overview, polls, a single poll, members
13// and rules.
14func Render(path string) string {
15 if members == nil {
16 return render.Heading(1, appTitle) + "This app is not initialized.\n"
17 }
18 segs, q := render.ParsePath(path)
19 switch {
20 case len(segs) == 0:
21 return renderHome()
22 case segs[0] == "polls" && len(segs) == 1:
23 return renderPolls(render.Page(q, render.PageCount(polls.Count(), pageSize)))
24 case segs[0] == "polls" && len(segs) == 2:
25 id, err := strconv.ParseUint(segs[1], 10, 64)
26 if err != nil {
27 return notFound()
28 }
29 return renderPoll(id)
30 case segs[0] == "members" && len(segs) == 1:
31 return renderMembers(render.Page(q, render.PageCount(members.Size(), pageSize)))
32 case segs[0] == "rules" && len(segs) == 1:
33 return renderRules()
34 }
35 return notFound()
36}
37
38func renderHome() string {
39 var sb strings.Builder
40 sb.WriteString(render.Heading(1, appTitle))
41 sb.WriteString("Members vote in single-choice polls. Each poll's electorate is frozen when it opens.\n\n")
42 sb.WriteString("- " + render.Link("Polls", render.RealmURL(pkgPath, "polls")) + " (" + strconv.Itoa(polls.Count()) + ")\n")
43 sb.WriteString("- " + render.Link("Members", render.RealmURL(pkgPath, "members")) + " (" + strconv.Itoa(members.Size()) + ")\n")
44 sb.WriteString("- " + render.Link("Rules", render.RealmURL(pkgPath, "rules")) + "\n\n")
45 sb.WriteString(render.TxLink("Join (requires an invitation)", pkgPath, "Join") + "\n\n")
46 sb.WriteString(renderPollTable(polls.List(0, 5)))
47 sb.WriteString("_Recipe " + render.Text(recipeID+"@"+recipeVersion) + ". Membership is address-based; one address is not one person._\n")
48 return sb.String()
49}
50
51func renderPolls(page int) string {
52 pages := render.PageCount(polls.Count(), pageSize)
53 return render.Heading(1, "Polls") +
54 renderPollTable(polls.List((page-1)*pageSize, pageSize)) +
55 render.Pager(pkgPath, "polls", page, pages)
56}
57
58func renderPollTable(list []core.BallotView) string {
59 if len(list) == 0 {
60 return "No polls yet.\n\n"
61 }
62 var rows [][]string
63 for _, v := range list {
64 rows = append(rows, render.TrustedRow(
65 render.Link("#"+strconv.FormatUint(v.ID, 10), render.RealmURL(pkgPath, "polls/"+strconv.FormatUint(v.ID, 10))),
66 render.Text(v.Title),
67 render.Text(statusLabel(v, time.Now().Unix())),
68 strconv.Itoa(v.Turnout),
69 ))
70 }
71 return render.Table([]string{"Poll", "Title", "Status", "Ballots"}, rows)
72}
73
74func renderPoll(id uint64) string {
75 v, ok := polls.Ballot(id)
76 if !ok {
77 return notFound()
78 }
79 now := time.Now().Unix()
80 idStr := strconv.FormatUint(id, 10)
81 var sb strings.Builder
82 sb.WriteString(render.Heading(1, v.Title))
83 sb.WriteString("Status: **" + render.Text(statusLabel(v, now)) + "**")
84 if v.Status != core.BallotDraft {
85 sb.WriteString(" · electorate " + strconv.Itoa(v.ElectorateSize) + " · ballots " + strconv.Itoa(v.Turnout))
86 sb.WriteString(" · deadline " + render.Text(time.Unix(v.Deadline, 0).UTC().Format(time.RFC3339)))
87 }
88 sb.WriteString("\n\n")
89
90 var rows [][]string
91 for i, opt := range v.Options {
92 action := ""
93 if v.Status == core.BallotOpen && now < v.Deadline {
94 action = render.TxLink("vote", pkgPath, "Vote", "id", idStr, "choice", strconv.Itoa(i))
95 }
96 rows = append(rows, render.TrustedRow(render.Text(opt), strconv.Itoa(v.Counts[i]), action))
97 }
98 sb.WriteString(render.Table([]string{"Option", "Votes", ""}, rows))
99
100 switch {
101 case v.Status == core.BallotDraft:
102 sb.WriteString(render.TxLink("Open this poll", pkgPath, "OpenPoll", "id", idStr) + "\n\n")
103 case v.Status == core.BallotOpen && now >= v.Deadline:
104 sb.WriteString(render.TxLink("Finalize result", pkgPath, "Finalize", "id", idStr) + "\n\n")
105 case v.Status == core.BallotFinalized:
106 sb.WriteString("Result: **" + render.Text(outcomeLabel(v)) + "**\n\n")
107 }
108 sb.WriteString(render.Link("All polls", render.RealmURL(pkgPath, "polls")) + "\n")
109 return sb.String()
110}
111
112func renderMembers(page int) string {
113 var rows [][]string
114 for _, m := range members.List((page-1)*pageSize, pageSize) {
115 rows = append(rows, render.Row(m.Address.String(), m.Role.String()))
116 }
117 return render.Heading(1, "Members") +
118 render.Table([]string{"Address", "Role"}, rows) +
119 render.Pager(pkgPath, "members", page, render.PageCount(members.Size(), pageSize))
120}
121
122func renderRules() string {
123 var rows [][]string
124 for _, r := range policy.List() {
125 rows = append(rows, render.Row(r.Op, r.Rule.String(), r.Floor.String()))
126 }
127 rows = append(rows, render.Row("poll.vote", "electorate frozen at opening (members)", "-"))
128 return render.Heading(1, "Rules") +
129 render.Table([]string{"Operation", "Who may", "Most permissive allowed"}, rows)
130}
131
132func statusLabel(v core.BallotView, now int64) string {
133 if v.Status == core.BallotOpen && now >= v.Deadline {
134 return "voting ended, awaiting finalization"
135 }
136 return v.Status.String()
137}
138
139func outcomeLabel(v core.BallotView) string {
140 if v.Outcome == core.OutcomeWinner && v.Winner >= 0 && v.Winner < len(v.Options) {
141 return "winner: " + v.Options[v.Winner]
142 }
143 return v.Outcome.String()
144}
145
146func notFound() string {
147 return render.Heading(1, "Not found") + render.Link("Back to the app", render.RealmURL(pkgPath, "")) + "\n"
148}