1package present23import (4 "net/url"5 "strconv"6 "strings"7 "time"89 "chain/runtime"1011 "gno.land/p/moul/md/v0"12 "gno.land/p/moul/mdtable/v0"13 "gno.land/p/moul/realmpath/v0"14 "gno.land/p/moul/txlink/v0"15 "gno.land/p/nt/avl/v0"16 "gno.land/p/nt/avl/pager/v0"17 "gno.land/p/nt/ownable/v0"18 "gno.land/p/nt/seqid/v0"19 "gno.land/p/nt/ufmt/v0"20)2122var chainDomain = runtime.ChainDomain()2324type Presentation struct {25 id string26 Slug string27 Title string28 Event string29 Author string30 Uploader address31 Date time.Time32 Content string33 EditDate time.Time34 NumSlides int35}3637var (38 byID avl.Tree // id -> *Presentation39 bySlug avl.Tree // slug -> *Presentation40 idgen seqid.ID41 Ownable *ownable.Ownable42)4344// owner is the realm admin allowed to add/update/delete presentations.45const owner address = "g1manfred47kzduec920z88wfr64ylksmdcedlf5" // @moul4647func init() {48 Ownable = ownable.NewWithAddress(owner)49}5051// Render handles the realm's rendering logic52func Render(path string) string {53 req := realmpath.Parse(path)5455 // Get slug from path56 slug := req.PathPart(0)5758 // List view (home)59 if slug == "" {60 return renderList(req)61 }6263 // Slides view64 if req.PathPart(1) == "slides" {65 page := 166 if pageStr := req.Query.Get("page"); pageStr != "" {67 var err error68 page, err = strconv.Atoi(pageStr)69 if err != nil {70 return "400: invalid page number"71 }72 }73 return renderSlides(slug, page)74 }7576 // Regular view77 return renderView(slug)78}7980// Set adds or updates a presentation81func Set(cur realm, slug, title, event, author, date, content string) string {82 caller := cur.Previous().Address()83 Ownable.AssertOwnedBy(caller)84 return setPresentation(caller, slug, title, event, author, date, content)85}8687// setPresentation is the shared record-mutation preamble used by both the88// owner-gated Set entrypoint and the init seeding.89func setPresentation(uploader address, slug, title, event, author, date, content string) string {90 parsedDate, err := time.Parse("2006-01-02", date)91 if err != nil {92 return "400: invalid date format (expected: YYYY-MM-DD)"93 }9495 numSlides := 1 // Count intro slide96 for _, line := range strings.Split(content, "\n") {97 if strings.HasPrefix(line, "## ") {98 numSlides++99 }100 }101 numSlides++ // Count thank you slide102103 // Reuse the existing id when the slug is already known (update in place).104 var id string105 if v := bySlug.Get(slug); v != nil {106 id = v.(*Presentation).id107 } else {108 id = idgen.Next().String()109 }110111 p := &Presentation{112 id: id,113 Slug: slug,114 Title: title,115 Event: event,116 Author: author,117 Uploader: uploader,118 Date: parsedDate,119 Content: content,120 EditDate: time.Now(),121 NumSlides: numSlides,122 }123124 byID.Set(id, p)125 bySlug.Set(slug, p)126 return "presentation saved successfully"127}128129// Delete removes a presentation130func Delete(cur realm, slug string) string {131 Ownable.AssertOwnedBy(cur.Previous().Address())132133 v := bySlug.Get(slug)134 if v == nil {135 return "404: presentation not found"136 }137 p := v.(*Presentation)138139 // XXX: consider this:140 // if p.Uploader != cur.Previous().Address() {141 // return "401: unauthorized - only the uploader can delete their presentations"142 // }143144 byID.Remove(p.id)145 bySlug.Remove(slug)146 return "presentation deleted successfully"147}148149func renderList(req *realmpath.Request) string {150 var out strings.Builder151 out.WriteString(md.H1("Presentations"))152153 // Setup pager over a tree sorted by the requested field.154 tree := buildSortTree(getSortField(req))155 pgr := pager.NewPager(tree, 10, isSortReversed(req))156157 // Get current page. The pager only reads the query string; pass just the158 // query so url.Parse is not tripped by the "/realm:slug" colon in the159 // full realm path.160 page := pgr.MustGetPageByPath("?" + req.Query.Encode())161162 // Create table163 dateColumn := renderSortLink(req, "date", "Date")164 titleColumn := renderSortLink(req, "title", "Title")165 authorColumn := renderSortLink(req, "author", "Author")166 table := mdtable.Table{167 Headers: []string{dateColumn, titleColumn, "Event", authorColumn, "Slides"},168 }169170 // Add rows from current page171 for _, item := range page.Items {172 p := item.Value.(*Presentation)173 table.Append([]string{174 p.Date.Format("2006-01-02"),175 md.Link(p.Title, localPath(p.Slug, nil)),176 p.Event,177 p.Author,178 ufmt.Sprintf("%d", p.NumSlides),179 })180 }181182 out.WriteString(table.String())183 out.WriteString(page.Picker("?" + req.Query.Encode()))184 return out.String()185}186187// buildSortTree builds an avl.Tree whose keys order the presentations by the188// requested field. A trailing id keeps keys unique and makes ties189// deterministic (insertion order).190func buildSortTree(field string) *avl.Tree {191 tree := avl.NewTree()192 byID.Iterate("", "", func(id string, v any) bool {193 p := v.(*Presentation)194 var key string195 switch field {196 case "title":197 key = p.Title + "\x00" + id198 case "author":199 key = p.Author + "\x00" + id200 default: // date (and any unknown field)201 key = p.Date.Format("2006-01-02") + "\x00" + id202 }203 tree.Set(key, p)204 return false205 })206 return tree207}208209func (p *Presentation) FirstSlide() string {210 var out strings.Builder211 out.WriteString(md.H1(p.Title))212 out.WriteString(md.Paragraph(md.Bold(p.Event) + ", " + p.Date.Format("2 Jan 2006")))213 out.WriteString(md.Paragraph("by " + md.Bold(p.Author))) // XXX: link to u/?214 return out.String()215}216217func (p *Presentation) LastSlide() string {218 var out strings.Builder219 out.WriteString(md.H1(p.Title))220 out.WriteString(md.H2("Thank You!"))221 out.WriteString(md.Paragraph(p.Author))222 fullPath := "https://" + chainDomain + localPath(p.Slug, nil)223 out.WriteString(md.Paragraph("🔗 " + md.Link(fullPath, fullPath)))224 // XXX: QRCode225 return out.String()226}227228func renderView(slug string) string {229 if slug == "" {230 return "400: missing presentation slug"231 }232233 v := bySlug.Get(slug)234 if v == nil {235 return "404: presentation not found"236 }237238 p := v.(*Presentation)239 var out strings.Builder240241 // Header using FirstSlide helper242 out.WriteString(p.FirstSlide())243244 // Slide mode link245 out.WriteString(md.Link("View as slides", localPath(p.Slug+"/slides", nil)) + "\n\n")246 out.WriteString(md.HorizontalRule())247 out.WriteString(md.Paragraph(p.Content))248249 // Metadata footer250 out.WriteString(md.HorizontalRule())251 out.WriteString(ufmt.Sprintf("Last edited: %s\n\n", p.EditDate.Format("2006-01-02 15:04:05")))252 out.WriteString(ufmt.Sprintf("Uploader: `%s`\n\n", p.Uploader))253 out.WriteString(ufmt.Sprintf("Number of slides: %d\n\n", p.NumSlides))254255 // Admin actions256 // XXX: consider a dynamic toggle for admin actions257 editLink := txlink.Call("Set",258 "slug", p.Slug,259 "title", p.Title,260 "author", p.Author,261 "event", p.Event,262 "date", p.Date.Format("2006-01-02"),263 )264 deleteLink := txlink.Call("Delete", "slug", p.Slug)265 out.WriteString(md.Paragraph(md.Link("Edit", editLink) + " | " + md.Link("Delete", deleteLink)))266267 return out.String()268}269270// renderSlidesNavigation returns the navigation bar for slides271func renderSlidesNavigation(slug string, currentPage, totalSlides int) string {272 var out strings.Builder273 if currentPage > 1 {274 prevLink := localPath(slug+"/slides", url.Values{"page": {ufmt.Sprintf("%d", currentPage-1)}})275 out.WriteString(md.Link("← Prev", prevLink) + " ")276 }277 out.WriteString(ufmt.Sprintf("| %d/%d |", currentPage, totalSlides))278 if currentPage < totalSlides {279 nextLink := localPath(slug+"/slides", url.Values{"page": {ufmt.Sprintf("%d", currentPage+1)}})280 out.WriteString(" " + md.Link("Next →", nextLink))281 }282 return md.Paragraph(out.String())283}284285func renderSlides(slug string, currentPage int) string {286 if slug == "" {287 return "400: missing presentation ID"288 }289290 v := bySlug.Get(slug)291 if v == nil {292 return "404: presentation not found"293 }294295 p := v.(*Presentation)296 slides := strings.Split("\n"+p.Content, "\n## ")297 if currentPage < 1 || currentPage > p.NumSlides {298 return "404: invalid slide number"299 }300301 var out strings.Builder302303 // Display current slide304 if currentPage == 1 {305 out.WriteString(p.FirstSlide())306 } else if currentPage == p.NumSlides {307 out.WriteString(p.LastSlide())308 } else {309 out.WriteString(md.H1(p.Title))310 out.WriteString("## " + slides[currentPage-1] + "\n\n")311 }312313 out.WriteString(renderSlidesNavigation(slug, currentPage, p.NumSlides))314 return out.String()315}316317// Helper functions for sorting and pagination318func getSortField(req *realmpath.Request) string {319 field := req.Query.Get("sort")320 switch field {321 case "date", "author", "title":322 return field323 }324 return "date"325}326327func isSortReversed(req *realmpath.Request) bool {328 return req.Query.Get("order") != "asc"329}330331func renderSortLink(req *realmpath.Request, field, label string) string {332 currentField := getSortField(req)333 currentOrder := req.Query.Get("order")334335 newOrder := "desc"336 if field == currentField && currentOrder != "asc" {337 newOrder = "asc"338 }339340 query := req.Query341 query.Set("sort", field)342 query.Set("order", newOrder)343344 if field == currentField {345 if newOrder == "asc" {346 label += " ↑"347 } else {348 label += " ↓"349 }350 }351352 return md.Link(label, "?"+query.Encode())353}354355// helper to create local realm links356func localPath(path string, query url.Values) string {357 req := &realmpath.Request{358 Path: path,359 Query: query,360 }361 return req.String()362}363Delete(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, slug string) string
Render(path string) string
Set(cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, slug string, title string, event string, author string, date string, content string) string
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.