diff --git a/examples/gno.land/r/demo/userbook/render.gno b/examples/gno.land/r/demo/userbook/render.gno new file mode 100644 index 00000000000..22d7f97eabd --- /dev/null +++ b/examples/gno.land/r/demo/userbook/render.gno @@ -0,0 +1,51 @@ +// Package userbook demonstrates a small userbook system working with gnoweb +package userbook + +import ( + "sort" + "strconv" + + "gno.land/p/demo/avl/pager" + "gno.land/p/demo/ufmt" + "gno.land/p/moul/txlink" +) + +func Render(path string) string { + p := pager.NewPager(signupsTree, 2) + page := p.MustGetPageByPath(path) + + out := "# Welcome to UserBook!\n\n" + + out += ufmt.Sprintf("## [Click here to sign up!](%s)\n\n", txlink.Call("SignUp")) + out += "---\n\n" + + var sorted sortedSignups + for _, item := range page.Items { + sorted = append(sorted, item.Value.(*Signup)) + } + + sort.Sort(sorted) + + for _, item := range sorted { + out += ufmt.Sprintf("- **User #%d - %s - signed up on %s**\n\n", item.ordinal, item.address.String(), item.timestamp.Format("02-01-2006 15:04:05")) + } + + out += "---\n\n" + out += "**Page " + strconv.Itoa(page.PageNumber) + " of " + strconv.Itoa(page.TotalPages) + "**\n\n" + out += page.Selector() // Repeat selector for ease of navigation + return out +} + +type sortedSignups []*Signup + +func (s sortedSignups) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s sortedSignups) Len() int { + return len(s) +} + +func (s sortedSignups) Less(i, j int) bool { + return s[i].timestamp.Before(s[j].timestamp) +} diff --git a/examples/gno.land/r/demo/userbook/userbook.gno b/examples/gno.land/r/demo/userbook/userbook.gno index c49bd90fa42..c958dc9e5b0 100644 --- a/examples/gno.land/r/demo/userbook/userbook.gno +++ b/examples/gno.land/r/demo/userbook/userbook.gno @@ -1,158 +1,46 @@ -// This realm demonstrates a small userbook system working with gnoweb +// Package userbook demonstrates a small userbook system working with gnoweb package userbook import ( "std" - "strconv" + "time" "gno.land/p/demo/avl" - "gno.land/p/demo/mux" "gno.land/p/demo/ufmt" ) type Signup struct { - account string - height int64 + address std.Address + ordinal int + timestamp time.Time } -// signups - keep a slice of signed up addresses efficient pagination -var signups []Signup +var signupsTree = avl.NewTree() -// tracker - keep track of who signed up -var ( - tracker *avl.Tree - router *mux.Router -) - -const ( - defaultPageSize = 20 - pathArgument = "number" - subPath = "page/{" + pathArgument + "}" - signUpEvent = "SignUp" -) +const signUpEvent = "SignUp" func init() { - // Set up tracker tree - tracker = avl.NewTree() - - // Set up route handling - router = mux.NewRouter() - router.HandleFunc("", renderHelper) - router.HandleFunc(subPath, renderHelper) - - // Sign up the deployer - SignUp() + SignUp() // Sign up the deployer } func SignUp() string { // Get transaction caller - caller := std.PrevRealm().Addr().String() - height := std.GetHeight() + caller := std.PrevRealm().Addr() // Check if the user is already signed up - if _, exists := tracker.Get(caller); exists { + if _, exists := signupsTree.Get(caller.String()); exists { panic(caller + " is already signed up!") } + now := time.Now() // Sign up the user - tracker.Set(caller, struct{}{}) - signup := Signup{ - caller, - height, - } - - signups = append(signups, signup) - std.Emit(signUpEvent, "SignedUpAccount", signup.account) + signupsTree.Set(caller.String(), &Signup{ + std.PrevRealm().Addr(), + signupsTree.Size(), + now, + }) - return ufmt.Sprintf("%s added to userbook up at block #%d!", signup.account, signup.height) -} - -func GetSignupsInRange(page, pageSize int) ([]Signup, int) { - if page < 1 { - panic("page number cannot be less than 1") - } - - if pageSize < 1 || pageSize > 50 { - panic("page size must be from 1 to 50") - } - - // Pagination - // Calculate indexes - startIndex := (page - 1) * pageSize - endIndex := startIndex + pageSize - - // If page does not contain any users - if startIndex >= len(signups) { - return nil, -1 - } - - // If page contains fewer users than the page size - if endIndex > len(signups) { - endIndex = len(signups) - } - - return signups[startIndex:endIndex], endIndex -} - -func renderHelper(res *mux.ResponseWriter, req *mux.Request) { - totalSignups := len(signups) - res.Write("# Welcome to UserBook!\n\n") - - // Get URL parameter - page, err := strconv.Atoi(req.GetVar("number")) - if err != nil { - page = 1 // render first page on bad input - } - - // Fetch paginated signups - fetchedSignups, endIndex := GetSignupsInRange(page, defaultPageSize) - // Handle empty page case - if len(fetchedSignups) == 0 { - res.Write("No users on this page!\n\n") - res.Write("---\n\n") - res.Write("[Back to Page #1](/r/demo/userbook:page/1)\n\n") - return - } - - // Write page title - res.Write(ufmt.Sprintf("## UserBook - Page #%d:\n\n", page)) - - // Write signups - pageStartIndex := defaultPageSize * (page - 1) - for i, signup := range fetchedSignups { - out := ufmt.Sprintf("#### User #%d - %s - signed up at Block #%d\n", pageStartIndex+i, signup.account, signup.height) - res.Write(out) - } - - res.Write("---\n\n") - - // Write UserBook info - latestSignupIndex := totalSignups - 1 - res.Write(ufmt.Sprintf("#### Total users: %d\n", totalSignups)) - res.Write(ufmt.Sprintf("#### Latest signup: User #%d at Block #%d\n", latestSignupIndex, signups[latestSignupIndex].height)) - - res.Write("---\n\n") - - // Write page number - res.Write(ufmt.Sprintf("You're viewing page #%d", page)) - - // Write navigation buttons - var prevPage string - var nextPage string - // If we are on any page that is not the first page - if page > 1 { - prevPage = ufmt.Sprintf(" - [Previous page](/r/demo/userbook:page/%d)", page-1) - } - - // If there are more pages after the current one - if endIndex < totalSignups { - nextPage = ufmt.Sprintf(" - [Next page](/r/demo/userbook:page/%d)\n\n", page+1) - } - - res.Write(prevPage) - res.Write(nextPage) -} + std.Emit(signUpEvent, "SignedUpAccount", caller.String()) -func Render(path string) string { - return router.Render(path) + return ufmt.Sprintf("%s added to userbook! Timestamp: %s", caller.String(), now.Format(time.RFC822Z)) } diff --git a/examples/gno.land/r/demo/userbook/userbook_test.gno b/examples/gno.land/r/demo/userbook/userbook_test.gno deleted file mode 100644 index 8d10d381e08..00000000000 --- a/examples/gno.land/r/demo/userbook/userbook_test.gno +++ /dev/null @@ -1,79 +0,0 @@ -package userbook - -import ( - "std" - "strings" - "testing" - - "gno.land/p/demo/testutils" - "gno.land/p/demo/ufmt" -) - -func TestRender(t *testing.T) { - // Sign up 20 users + deployer - for i := 0; i < 20; i++ { - addrName := ufmt.Sprintf("test%d", i) - caller := testutils.TestAddress(addrName) - std.TestSetOrigCaller(caller) - SignUp() - } - - testCases := []struct { - name string - nextPage bool - prevPage bool - path string - expectedNumberOfUsers int - }{ - { - name: "1st page render", - nextPage: true, - prevPage: false, - path: "page/1", - expectedNumberOfUsers: 20, - }, - { - name: "2nd page render", - nextPage: false, - prevPage: true, - path: "page/2", - expectedNumberOfUsers: 1, - }, - { - name: "Invalid path render", - nextPage: true, - prevPage: false, - path: "page/invalidtext", - expectedNumberOfUsers: 20, - }, - { - name: "Empty Page", - nextPage: false, - prevPage: false, - path: "page/1000", - expectedNumberOfUsers: 0, - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - got := Render(tc.path) - numUsers := countUsers(got) - - if tc.prevPage && !strings.Contains(got, "Previous page") { - t.Fatalf("expected to find Previous page, didn't find it") - } - if tc.nextPage && !strings.Contains(got, "Next page") { - t.Fatalf("expected to find Next page, didn't find it") - } - - if tc.expectedNumberOfUsers != numUsers { - t.Fatalf("expected %d, got %d users", tc.expectedNumberOfUsers, numUsers) - } - }) - } -} - -func countUsers(input string) int { - return strings.Count(input, "#### User #") -}