Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(home-realm): add interactive home realm example #2918

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
7 changes: 7 additions & 0 deletions examples/gno.land/r/stefann/home/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module gno.land/r/stefann/home

require (
gno.land/p/demo/ufmt v0.0.0-latest
gno.land/p/demo/ownable v0.0.0-latest
gno.land/r/stefann/registry v0.0.0-latest
)
277 changes: 277 additions & 0 deletions examples/gno.land/r/stefann/home/home.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
package home
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add unit tests for this realm?


import (
"sort"
"std"

"gno.land/p/demo/ownable"
"gno.land/p/demo/ufmt"

"gno.land/r/stefann/registry"
)

type City struct {
Name string
URL string
}

type Sponsor struct {
Address std.Address
Amount std.Coins
}

var (
pfp string
cities []City
currentCityIndex int
aboutMe [2]string
jarLink string
maxSponsors int
sponsors []Sponsor
totalDonated std.Coins
totalDonations int
owner *ownable.Ownable
)

func init() {
owner = ownable.NewWithAddress(registry.MainAddr())
pfp = "https://i.ibb.co/Bc5YNCx/DSC-0095a.jpg"
cities = []City{
{Name: "Venice", URL: "https://i.ibb.co/1mcZ7b1/venice.jpg"},
{Name: "Tokyo", URL: "https://i.ibb.co/wNDJv3H/tokyo.jpg"},
{Name: "São Paulo", URL: "https://i.ibb.co/yWMq2Sn/sao-paulo.jpg"},
{Name: "Toronto", URL: "https://i.ibb.co/pb95HJB/toronto.jpg"},
{Name: "Bangkok", URL: "https://i.ibb.co/pQy3w2g/bangkok.jpg"},
{Name: "New York", URL: "https://i.ibb.co/6JWLm0h/new-york.jpg"},
{Name: "Paris", URL: "https://i.ibb.co/q9vf6Hs/paris.jpg"},
{Name: "Kandersteg", URL: "https://i.ibb.co/60DzywD/kandersteg.jpg"},
{Name: "Rothenburg", URL: "https://i.ibb.co/cr8d2rQ/rothenburg.jpg"},
{Name: "Capetown", URL: "https://i.ibb.co/bPGn0v3/capetown.jpg"},
{Name: "Sydney", URL: "https://i.ibb.co/TBNzqfy/sydney.jpg"},
{Name: "Oeschinen Lake", URL: "https://i.ibb.co/QJQwp2y/oeschinen-lake.jpg"},
{Name: "Barra Grande", URL: "https://i.ibb.co/z4RXKc1/barra-grande.jpg"},
{Name: "London", URL: "https://i.ibb.co/CPGtvgr/london.jpg"},
}
currentCityIndex = 0
jarLink = "https://TODO" // This value should be injected through UpdateJarLink after deployment.
maxSponsors = 5
aboutMe = [2]string{
`<h3 style="font-size: 1.4em;">About Me</h3>
<p style="font-size: 1.1em;">Hey there! I’m Stefan, a student of Computer Science. I’m all about exploring and adventure — whether it’s diving into the latest tech or discovering a new city, I’m always up for the challenge!</p>`,
`<h3 style="font-size: 1.4em;">Contributions</h3>
<p style="font-size: 1.1em;">I'm just getting started, but you can follow my journey through Gno.land right here <a href="https://github.com/gnolang/hackerspace/issues/94" target="_blank">🔗</a></p>`,
}
}

func UpdateMaxSponsors(newMax int) {
owner.AssertCallerIsOwner()
maxSponsors = newMax
}

func UpdateCities(newCities []City) {
owner.AssertCallerIsOwner()
cities = newCities
}

func UpdateJarLink(newLink string) {
owner.AssertCallerIsOwner()
jarLink = newLink
}

func UpdatePFP(url string) {
owner.AssertCallerIsOwner()
pfp = url
}

func UpdateAboutMe(col1, col2 string) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why limit the about section to 2 columns?

owner.AssertCallerIsOwner()
aboutMe[0] = col1
aboutMe[1] = col2
}

func Donate() {
address := std.GetOrigCaller()
amount := std.GetOrigSend()

if amount.AmountOf("ugnot") == 0 {
panic("Donation must include GNOT")
}

found := false

for i, sponsor := range sponsors {
if sponsor.Address == address {
sponsors[i].Amount = sponsors[i].Amount.Add(amount)
found = true
break
}
}

if !found {
sponsors = append(sponsors, Sponsor{Address: address, Amount: amount})
}
Comment on lines +100 to +112
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use a sponsor map, instead of always iterating through the list?


totalDonated.Add(amount)

totalDonations++

sortSponsorsByAmount()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should the donator have to pay for sorting the slice?


if len(cities) > 0 {
currentCityIndex++
if currentCityIndex >= len(cities) {
currentCityIndex = 0
Copy link
Member

@moul moul Oct 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can be achieved in a single line with %.

btw, should be done when rendering, not when setting; so that if you call UpdateCity with a shorter list, you contract won't break.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For context, what @moul is suggesting:

image := images[index%len(images)]

No need to reset the counter like this manually, the power of math is on your side

}
}
}

type SponsorSlice []Sponsor

func (s SponsorSlice) Len() int {
return len(s)
}

func (s SponsorSlice) Less(i, j int) bool {
return s[i].Amount.AmountOf("ugnot") > s[j].Amount.AmountOf("ugnot")
}

func (s SponsorSlice) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}

func sortSponsorsByAmount() {
sort.Sort(SponsorSlice(sponsors))
}

func GetTopSponsors() []Sponsor {
return sponsors
}
Comment on lines +128 to +148
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can just keep the top 5 / 10 sponsors in a map, and sort it on each render -- it shouldn't be a problem


func CollectDonations() {
owner.AssertCallerIsOwner()

banker := std.GetBanker(std.BankerTypeRealmSend)

ownerAddr := registry.MainAddr()
banker.SendCoins(std.CurrentRealm().Addr(), ownerAddr, banker.GetCoins(std.CurrentRealm().Addr()))
}

func GetTotalDonations() std.Coins {
return totalDonated
}

func GetDonationCount() int {
return totalDonations
}
Comment on lines +159 to +165
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a complete overlap between sponsors, and donations. Why separate these two concepts?


func Render(path string) string {
out := ufmt.Sprintf("# Exploring %s!\n\n", cities[currentCityIndex].Name)

out += renderAboutMe()
out += "\n\n"
out += renderTips()

return out
}

func renderAboutMe() string {
out := "<div class='rows-3'>"

out += "<div style='position: relative; text-align: center;'>\n\n"

out += ufmt.Sprintf("<div style='background-image: url(%s); background-size: cover; background-position: center; width: 100%%; height: 600px; position: relative; border-radius: 15px; overflow: hidden;'>\n\n", cities[currentCityIndex].URL)

out += ufmt.Sprintf("<img src='%s' alt='my profile pic' style='width: 250px; height: auto; aspect-ratio: 1 / 1; object-fit: cover; border-radius: 50%%; border: 3px solid #1e1e1e; position: absolute; top: 75%%; left: 50%%; transform: translate(-50%%, -50%%);'>\n\n", pfp)

out += "</div>\n\n"

out += "<div>\n\n"
out += aboutMe[0] + "\n\n"
out += "</div>\n\n"

out += "<div>\n\n"
out += aboutMe[1] + "\n\n"
out += "</div>\n\n"

out += "</div><!-- /rows-3 -->\n\n"

return out
}

func renderTips() string {
out := `<div class="jumbotron" style="display: flex; flex-direction: column; justify-content: flex-start; align-items: center; padding-top: 40px; padding-bottom: 50px; text-align: center;">` + "\n\n"

out += `<div class="rows-2" style="max-width: 500px; width: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center;">` + "\n"

out += `<h1 style="margin-bottom: 50px;">Help Me Travel The World</h1>` + "\n\n"

out += renderTipsJar() + "\n"

out += ufmt.Sprintf(`<strong style="font-size: 1.2em;">I am currently in %s, <br> tip the jar to send me somewhere else!</strong>`, cities[currentCityIndex].Name)

out += `<br><span style="font-size: 1.2em; font-style: italic; margin-top: 10px; display: inline-block;">Click the jar, tip in GNOT coins, and watch my background change as I head to a new adventure!</span></p>` + "\n\n"

out += renderSponsors()

out += `</div><!-- /rows-2 -->` + "\n\n"

out += `</div><!-- /jumbotron -->` + "\n"

return out
}

func formatAddress(address string) string {
if len(address) <= 8 {
return address
}
return address[:4] + "..." + address[len(address)-4:]
}

func renderSponsors() string {
out := `<h3 style="margin-top: 5px; margin-bottom: 20px">Sponsor Leaderboard</h3>` + "\n"

if len(sponsors) == 0 {
out += `<p style="text-align: center;">No sponsors yet. Be the first to tip the jar!</p>` + "\n"
} else {
numSponsors := len(sponsors)
if numSponsors > maxSponsors {
numSponsors = maxSponsors
}

out += `<ul style="list-style-type: none; padding: 0; border: 1px solid #ddd; border-radius: 8px; width: 100%; max-width: 300px; margin: 0 auto;">` + "\n"

for i := 0; i < numSponsors; i++ {
sponsor := sponsors[i]
isLastItem := (i == numSponsors-1)

padding := "10px 5px"
border := "border-bottom: 1px solid #ddd;"

if isLastItem {
padding = "8px 5px"
border = ""
}

out += ufmt.Sprintf(
`<li style="padding: %s; %s text-align: left;">
<strong style="padding-left: 5px;">%d. %s</strong>
<span style="float: right; padding-right: 5px;">%s</span>
</li>`,
padding, border, i+1, formatAddress(sponsor.Address.String()), sponsor.Amount.String(),
)
}

}

return out
}

func renderTipsJar() string {
out := ufmt.Sprintf(`<a href="%s" target="_blank" style="display: block; text-decoration: none;">`, jarLink) + "\n"

out += `<img src="https://i.ibb.co/4TH9zbw/tips-jar.png" alt="Tips Jar" style="width: 300px; height: auto; display: block; margin: 0 auto;">` + "\n"

out += `</a>` + "\n"

return out
}
Comment on lines +269 to +277
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you reconstruct this string every time, if it doesn't change?
Isn't it a constant?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I explained the purpose of jarLink and why it can't be a constant in the comment below. #2918 (comment)

Or did you mean to simplify the string concatenation by building the entire string in one call to ufmt.Sprintf? It's done this way for clarity, but I can change it if needed.

5 changes: 5 additions & 0 deletions examples/gno.land/r/stefann/registry/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module gno.land/r/stefann/registry

require (
gno.land/p/demo/ownable v0.0.0-latest
)
51 changes: 51 additions & 0 deletions examples/gno.land/r/stefann/registry/registry.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package registry

import (
"errors"
"std"

"gno.land/p/demo/ownable"
)

var (
mainAddr std.Address
backupAddr std.Address
owner *ownable.Ownable
)

func init() {
mainAddr = "g1sd5ezmxt4rwpy52u6wl3l3y085n8x0p6nllxm8"
backupAddr = "g13awn2575t8s2vf3svlprc4dg0e9z5wchejdxk8"

owner = ownable.NewWithAddress(mainAddr)
}

func MainAddr() std.Address {
return mainAddr
}

func BackupAddr() std.Address {
return backupAddr
}

func SetMainAddr(addr std.Address) error {
if !addr.IsValid() {
return errors.New("config: invalid address")
}

owner.AssertCallerIsOwner()

mainAddr = addr
return nil
}

func SetBackupAddr(addr std.Address) error {
if !addr.IsValid() {
return errors.New("config: invalid address")
}

owner.AssertCallerIsOwner()

backupAddr = addr
return nil
}