Skip to content

Commit

Permalink
feat(examples): add interactive realm r/stefann/home (#2918)
Browse files Browse the repository at this point in the history
# Description:

This PR finalizes the core functionality of my home realm project,
focusing on a dynamic and interactive home realm experience, driven by
GNOT donations. The following key features and enhancements are
introduced in this PR:

## Key Features:

- **Dynamic Background Change:**
- Implemented sequential background changes triggered by GNOT donations.
Each donation updates the city background in a fixed order, cycling
through a predefined set of cities. The background change is seamless,
offering a "traveling" experience for donors.

- **Sponsor Leaderboard:**
- Added a sponsor leaderboard to showcase the top contributors based on
their GNOT donations. The list displays the addresses of the top
sponsors, formatted for readability (first and last characters with
ellipses), and limits the displayed sponsors to the configurable
`maxSponsors` setting.

- **Donation Validation:**
- Introduced a strict GNOT validation check. The system now rejects any
donation attempts that don't include GNOT, ensuring only valid
contributions update the state of the realm.

- **Owner Withdrawal of Donations:**
- Implemented a feature that allows the realm owner to withdraw
accumulated GNOT donations, providing control over the funds contributed
by supporters.

- **Home Realm Configurations:**
- **Cities:** Cities can be dynamically updated to refresh the possible
backgrounds.
- **Maximum Sponsors:** Admins can configure the number of sponsors
shown on the leaderboard with the `maxSponsors` setting.
    - **Jar Link:** Admins can update the link to the donation jar.
  • Loading branch information
stefann-01 authored Oct 22, 2024
1 parent 4b68712 commit ec222ec
Show file tree
Hide file tree
Showing 5 changed files with 657 additions and 0 deletions.
9 changes: 9 additions & 0 deletions examples/gno.land/r/stefann/home/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
module gno.land/r/stefann/home

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

import (
"sort"
"std"
"strings"

"gno.land/p/demo/avl"
"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
}

type Profile struct {
pfp string
aboutMe []string
}

type Travel struct {
cities []City
currentCityIndex int
jarLink string
}

type Sponsorship struct {
maxSponsors int
sponsors *avl.Tree
DonationsCount int
sponsorsCount int
}

var (
profile Profile
travel Travel
sponsorship Sponsorship
owner *ownable.Ownable
)

func init() {
owner = ownable.NewWithAddress(registry.MainAddr())

profile = Profile{
pfp: "https://i.ibb.co/Bc5YNCx/DSC-0095a.jpg",
aboutMe: []string{
`### About Me`,
`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!`,

`### Contributions`,
`I'm just getting started, but you can follow my journey through Gno.land right [here](https://github.com/gnolang/hackerspace/issues/94) 🔗`,
},
}

travel = Travel{
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.
}

sponsorship = Sponsorship{
maxSponsors: 5,
sponsors: avl.NewTree(),
DonationsCount: 0,
sponsorsCount: 0,
}
}

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

func AddCities(newCities ...City) {
owner.AssertCallerIsOwner()

travel.cities = append(travel.cities, newCities...)
}

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

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

func UpdateAboutMe(aboutMeStr string) {
owner.AssertCallerIsOwner()
profile.aboutMe = strings.Split(aboutMeStr, "|")
}

func AddAboutMeRows(newRows ...string) {
owner.AssertCallerIsOwner()

profile.aboutMe = append(profile.aboutMe, newRows...)
}

func UpdateMaxSponsors(newMax int) {
owner.AssertCallerIsOwner()
if newMax <= 0 {
panic("maxSponsors must be greater than zero")
}
sponsorship.maxSponsors = newMax
}

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

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

existingAmount, exists := sponsorship.sponsors.Get(address.String())
if exists {
updatedAmount := existingAmount.(std.Coins).Add(amount)
sponsorship.sponsors.Set(address.String(), updatedAmount)
} else {
sponsorship.sponsors.Set(address.String(), amount)
sponsorship.sponsorsCount++
}

travel.currentCityIndex++
sponsorship.DonationsCount++

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

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 GetTopSponsors() []Sponsor {
var sponsorSlice SponsorSlice

sponsorship.sponsors.Iterate("", "", func(key string, value interface{}) bool {
addr := std.Address(key)
amount := value.(std.Coins)
sponsorSlice = append(sponsorSlice, Sponsor{Address: addr, Amount: amount})
return false
})

sort.Sort(sponsorSlice)
return sponsorSlice
}

func GetTotalDonations() int {
total := 0
sponsorship.sponsors.Iterate("", "", func(key string, value interface{}) bool {
total += int(value.(std.Coins).AmountOf("ugnot"))
return false
})
return total
}

func Render(path string) string {
out := ufmt.Sprintf("# Exploring %s!\n\n", travel.cities[travel.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", travel.cities[travel.currentCityIndex%len(travel.cities)].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", profile.pfp)

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

for _, rows := range profile.aboutMe {
out += "<div>\n\n"
out += rows + "\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>`, travel.cities[travel.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 sponsorship.sponsorsCount == 0 {
return out + `<p style="text-align: center;">No sponsors yet. Be the first to tip the jar!</p>` + "\n"
}

topSponsors := GetTopSponsors()
numSponsors := len(topSponsors)
if numSponsors > sponsorship.maxSponsors {
numSponsors = sponsorship.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 := topSponsors[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;">`, travel.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
}
Loading

0 comments on commit ec222ec

Please sign in to comment.