-
Notifications
You must be signed in to change notification settings - Fork 370
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
base: master
Are you sure you want to change the base?
Changes from all commits
54fd216
70d8ac3
47c0b81
f05370c
dbda3cf
aeb8340
b79ba55
b0ae489
d5c2fe4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,277 @@ | ||
package home | ||
|
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For context, what @moul is suggesting:
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I explained the purpose of 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. |
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 | ||
) |
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 | ||
} |
There was a problem hiding this comment.
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?