render.gno
3.58 Kb · 128 lines
1package kitindexdemo
2
3import (
4 "strconv"
5
6 "gno.land/p/moul/kit/store/v0"
7 "gno.land/p/moul/kit/ui/v0"
8 "gno.land/p/moul/md/v0"
9 "gno.land/p/moul/realmpath/v0"
10)
11
12// realmURL is this realm as gnoweb serves it. Links are absolute because the
13// path ends in /v0: a relative "./kitindexdemo:tag/x" resolves against the
14// parent directory to .../kitindexdemo/kitindexdemo:tag/x, which does not
15// exist. Not derived at runtime: a Render has no cur, and CurrentRealm there
16// is the caller. TestRealmURLMatchesTheModule pins it to the module line.
17const realmURL = "/r/moul/x/kitindexdemo/v0"
18
19// Render is the whole public surface. Three pages: the tag list, one tag, one
20// author.
21//
22// Every row goes through ui.NewTable rather than concatenated pipes, and every
23// caller string through ui.Cell, because a public path is immutable and a
24// Render that ships wrong ships forever.
25func Render(path string) string {
26 req := realmpath.Parse(path)
27 switch req.PathPart(0) {
28 case "":
29 return renderIndex(req)
30 case "tag":
31 return renderTag(req.PathPart(1))
32 case "author":
33 return renderAuthor(req.PathPart(1))
34 default:
35 return md.H1("Not found") + ui.Empty("No such page.")
36 }
37}
38
39func renderIndex(req *realmpath.Request) string {
40 // The page number is user input: clamp it into 1..pages, or ?page=-1
41 // renders an empty board under a footer saying "page -1 of 2".
42 pages := byTag.Pages(PageSize)
43 page := 1
44 if n, err := strconv.Atoi(req.Query.Get("page")); err == nil {
45 page = n
46 }
47 if page < 1 {
48 page = 1
49 }
50 if page > pages {
51 page = pages
52 }
53
54 t := ui.NewTable("tag", "notes")
55 for _, e := range byTag.Page(page, PageSize) {
56 t.Row(
57 md.Link(e.Key, realmURL+":tag/"+e.Key),
58 strconv.Itoa(len(e.IDs)),
59 )
60 }
61
62 return ui.Join("\n",
63 md.H1("Notes by tag"),
64 md.Paragraph(strconv.Itoa(notes.Len())+" note(s) under "+
65 strconv.Itoa(byTag.Keys())+" tag(s), by "+
66 strconv.Itoa(byAuthor.Keys())+" author(s)."),
67 t.OrEmpty("Nothing posted yet."),
68 pager(page, pages),
69 md.Paragraph(ui.Action("Post a note", "Post", "tag", "", "body", "")),
70 )
71}
72
73func renderTag(tag string) string {
74 if tag == "" {
75 return md.H1("Not found") + ui.Empty("No tag given.")
76 }
77 return md.H1("#"+ui.Cell(tag)) + notesTable(byTag.Lookup(tag), "No notes under this tag.")
78}
79
80func renderAuthor(addr string) string {
81 if addr == "" {
82 return md.H1("Not found") + ui.Empty("No author given.")
83 }
84 return md.H1("Notes by "+ui.AddrOf(addr)) +
85 notesTable(byAuthor.Lookup(addr), "This address has posted nothing.")
86}
87
88// notesTable renders a set of ids as rows, skipping any id the store no longer
89// holds.
90//
91// Skipping rather than panicking: an index and a store are two containers, and
92// a reader should see the records that exist rather than a page that aborts.
93// Retract keeps the two in step, so a miss here means a bug worth surviving,
94// not worth hiding.
95func notesTable(hits []store.ID, empty string) string {
96 t := ui.NewTable("#", "author", "tag", "note", "height")
97 for _, id := range hits {
98 v, ok := notes.Get(id)
99 if !ok {
100 continue
101 }
102 n := v.(*note)
103 t.Row(
104 id.String(),
105 ui.Addr(n.Author),
106 md.InlineCode(n.Tag),
107 ui.Cell(n.Body),
108 strconv.FormatInt(n.Height, 10),
109 )
110 }
111 return t.OrEmpty(empty)
112}
113
114// pager renders the previous/next links, or nothing when there is one page.
115func pager(page, pages int) string {
116 if pages < 2 {
117 return ""
118 }
119 out := ""
120 if page > 1 {
121 out += md.Link("previous", realmURL+":?page="+strconv.Itoa(page-1)) + " "
122 }
123 out += "page " + strconv.Itoa(page) + " of " + strconv.Itoa(pages)
124 if page < pages {
125 out += " " + md.Link("next", realmURL+":?page="+strconv.Itoa(page+1))
126 }
127 return out
128}