PathrockNetwork Gno Explorer
HomeBlocksTransactionsRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/r/moul/present/v0

Realm
Open in gnoweb ↗

Overview

Kind
Realm (renderable)
Name
v0
Namespace
moul / present
Files
4 (README)(gnomod.toml)
Exported functions
3
Module
gno.land/r/moul/present/v0
gno
0.9

Files (4)

  • README.mdmarkdown
  • gnomod.tomltoml
  • present_init.gnogno
  • present.gnogno
present.gnogno
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}363

Functions

  • Delete(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.

Rendered

RenderedRawgnoweb ↗

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.