Skip to content

Commit

Permalink
feat(example): r/mouss (gnolang#3472)
Browse files Browse the repository at this point in the history
Hi Gnomes ,
This is my home page. it was difficult to be creative 😅 , so i did what
i can do
### Home :
I put some informations about me 

![Screenshot 2025-01-09 at 21 14
20](https://github.com/user-attachments/assets/6da7926f-7b89-4363-83b0-83d103232f10)

### World kitchen :
I'm passionate about cooking, so I've set up a page for those who want
to share their national culinary specialties or just their favorite
recipes, since there are so many nationalities in `gno` community.

![Screenshot 2025-01-09 at 21 22
54](https://github.com/user-attachments/assets/f82b543e-43c4-47ac-8bab-4fe2874378a3)

feel free to add your recipes ;)

![Screenshot 2025-01-09 at 21 35
17](https://github.com/user-attachments/assets/1773522b-0b5a-431d-b8af-12ab6dc1c96e)

@leohhhn

---------

Co-authored-by: Leon Hudak <[email protected]>
Co-authored-by: mous1985 <[email protected]>
  • Loading branch information
3 people authored Mar 3, 2025
1 parent fc359ea commit 9cd5c0e
Show file tree
Hide file tree
Showing 6 changed files with 477 additions and 0 deletions.
37 changes: 37 additions & 0 deletions examples/gno.land/r/mouss/config/config.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package config

import (
"errors"
"std"

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

var (
OwnableMain = ownable.NewWithAddress("g1wq2h93ppkf2gkgncz5unayrsmt7pl8npktnznd")
OwnableBackup = ownable.NewWithAddress("g1hrfvdh7jdvnlxpk2y20tp3scj9jqal3zzu7wjz")

ErrUnauthorized = errors.New("config: unauthorized")
)

func SetMainAddr(addr std.Address) error {
return OwnableMain.TransferOwnership(addr)
}

func SetBackupAddr(addr std.Address) error {
return OwnableBackup.TransferOwnership(addr)
}

func IsAuthorized(addr std.Address) bool {
return addr == OwnableMain.Owner() || addr == OwnableBackup.Owner()
}

func Render(path string) string {
out := "# mouss configuration\n\n"

out += "## Authorized Addresses\n\n"
out += "- main: " + OwnableMain.Owner().String() + "\n"
out += "- backup: " + OwnableBackup.Owner().String() + "\n\n"

return out
}
88 changes: 88 additions & 0 deletions examples/gno.land/r/mouss/config/config_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package config

import (
"std"
"testing"

"gno.land/p/demo/testutils"
"gno.land/p/demo/uassert"
"gno.land/p/demo/urequire"
)

var (
mainAddr = std.Address("g1wq2h93ppkf2gkgncz5unayrsmt7pl8npktnznd")
backupAddr = std.Address("g1hrfvdh7jdvnlxpk2y20tp3scj9jqal3zzu7wjz")

addr1 = testutils.TestAddress("addr1")
addr2 = testutils.TestAddress("addr2")
addr3 = testutils.TestAddress("addr3")
)

func TestInitialOwnership(t *testing.T) {
uassert.Equal(t, OwnableMain.Owner(), mainAddr)
uassert.Equal(t, OwnableBackup.Owner(), backupAddr)
}

func TestIsAuthorized(t *testing.T) {
tests := []struct {
name string
addr std.Address
want bool
}{
{"main address is authorized", mainAddr, true},
{"backup address is authorized", backupAddr, true},
{"random address not authorized", addr3, false},
{"empty address not authorized", "", false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := IsAuthorized(tt.addr)
uassert.Equal(t, got, tt.want)
})
}
}

func TestSetMainAddr(t *testing.T) {
std.TestSetOriginCaller(mainAddr)

// Test successful transfer
err := SetMainAddr(addr2)
urequire.NoError(t, err)
uassert.Equal(t, OwnableMain.Owner(), addr2)

// Test unauthorized transfer
std.TestSetOriginCaller(addr3)
err = SetMainAddr(addr1)
uassert.ErrorContains(t, err, "ownable: caller is not owner")

// Test invalid address
std.TestSetOriginCaller(addr2)
err = SetMainAddr("")
uassert.ErrorContains(t, err, "ownable: new owner address is invalid")

// Reset state
std.TestSetOriginCaller(addr2)
err = SetMainAddr(mainAddr)
urequire.NoError(t, err)
}

func TestSetBackupAddr(t *testing.T) {
std.TestSetOriginCaller(backupAddr)

err := SetBackupAddr(addr2)
urequire.NoError(t, err)
uassert.Equal(t, OwnableBackup.Owner(), addr2)

std.TestSetOriginCaller(addr3)
err = SetBackupAddr(addr1)
uassert.ErrorContains(t, err, "ownable: caller is not owner")

std.TestSetOriginCaller(addr2)
err = SetBackupAddr("")
uassert.ErrorContains(t, err, "ownable: new owner address is invalid")

std.TestSetOriginCaller(addr2)
err = SetBackupAddr(backupAddr)
urequire.NoError(t, err)
}
1 change: 1 addition & 0 deletions examples/gno.land/r/mouss/config/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/r/mouss/config
1 change: 1 addition & 0 deletions examples/gno.land/r/mouss/home/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/r/mouss/home
251 changes: 251 additions & 0 deletions examples/gno.land/r/mouss/home/home.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
package home

import (
"std"
"strconv"
"strings"

"gno.land/p/demo/avl"
"gno.land/p/demo/mux"
"gno.land/p/demo/ufmt"
"gno.land/p/moul/addrset"
"gno.land/p/moul/md"
"gno.land/r/leon/hof"
"gno.land/r/mouss/config"
)

// Profile represents my personal profile information.
type Profile struct {
AboutMe string
Avatar string
Email string
Github string
LinkedIn string
Followers *addrset.Set // Set of followers addresses.
}

// Recipe represents a cooking recipe with its details.
type Recipe struct {
Name string
Origin string
Author std.Address
Ingredients string
Instructions string
Tips string
}

const (
realmURL = "/r/mouss/home"
rec = realmURL + ":recipe/"
gnoArt = `
-==++.
*@@@@= @- -@
#@@@@@: -==-.-- :-::===: .-++-. @- .===:.- .-.-==- .===:=@
#@@@@@@@: -@@%**%@@ #@@#*#@@- *@@**@@* @- +%=::-*@ +@=-:-@* +%=::-*@
+@%#**#%@@ %@+ :@@ *@+ #@=+@% %@+ @= :@: -@ +% +%.@: -@
-: - *@%:..+@@ *@+ #@=-@@: :@@= @- .@= =@ +@ *%.@= =@
--:==+=-:=. =%@%#*@@ *@+ #@+ =%@%%@%= #* %#=.:%*===*@ +% +% -%*===*@
-++++=++++. =-:::*@# . . .::. .. :: .:: . . .:: .
.-=+++=: .*###%#=
::
`
)

var (
router = mux.NewRouter()
profile Profile
recipes = avl.NewTree()
margheritaPizza *Recipe
)

// init initializes the router with the home page and recipe routes
// sets up my profile information, and my recipe
// and registers the home page in the hall of fame.
func init() {
router.HandleFunc("", renderHomepage)
router.HandleFunc("recipe/", renderRecipes)
router.HandleFunc("recipe/{name}", renderRecipe)
profile = Profile{
AboutMe: "👋 I'm Mustapha, a contributor to gno.land project from France. I'm passionate about coding, exploring new technologies, and contributing to open-source projects. Besides my tech journey, I'm also a pizzaiolo 🍕 who loves cooking and savoring good food.",
Avatar: "https://github.com/mous1985/assets/blob/master/avatar.png?raw=true",
Email: "[email protected]",
Github: "https://github.com/mous1985",
LinkedIn: "https://www.linkedin.com/in/mustapha-benazzouz-88646887/",
Followers: &addrset.Set{},
}
margheritaPizza = &Recipe{
Name: "Authentic Margherita Pizza 🤌",
Origin: "Naples, 🇮🇹",
Author: config.OwnableMain.Owner(),
Ingredients: " 1kg 00 flour\n 500ml water\n 3g fresh yeast\n 20g sea salt\n San Marzano tomatoes\n Fresh buffalo mozzarella\n Fresh basil\n Extra virgin olive oil",
Instructions: " Mix flour and water until incorporated\n Add yeast and salt, knead for 20 minutes\n Let rise for 2 hours at room temperature\n Divide into 250g balls\n Cold ferment for 24-48 hours\n Shape by hand, being gentle with the dough\n Top with crushed tomatoes, torn mozzarella, and basil\n Cook at 450°C for 60-90 seconds",
Tips: "Use a pizza steel or stone preheated for at least 1 hour. The dough should be soft and extensible. For best results, cook in a wood-fired oven.",
}
hof.Register()
}

// AddRecipe adds a new recipe in recipe page by users
func AddRecipe(name, origin, ingredients, instructions, tips string) string {
if err := validateRecipe(name, ingredients, instructions); err != nil {
panic(err)
}
recipe := &Recipe{
Name: name,
Origin: origin,
Author: std.PreviousRealm().Address(),
Ingredients: ingredients,
Instructions: instructions,
Tips: tips,
}
recipes.Set(name, recipe)
return "Recipe added successfully"
}

func UpdateAboutMe(about string) error {
if !config.IsAuthorized(std.PreviousRealm().Address()) {
panic(config.ErrUnauthorized)
}
profile.AboutMe = about
return nil
}

func UpdateAvatar(avatar string) error {
if !config.IsAuthorized(std.PreviousRealm().Address()) {
panic(config.ErrUnauthorized)
}
profile.Avatar = avatar
return nil
}

// validateRecipe checks if the provided recipe details are valid.
func validateRecipe(name, ingredients, instructions string) error {
if name == "" {
return ufmt.Errorf("recipe name cannot be empty")
}
if len(ingredients) == 0 {
return ufmt.Errorf("ingredients cannot be empty")
}
if len(instructions) == 0 {
return ufmt.Errorf("instructions cannot be empty")
}
return nil
}

// Follow allows a users to follow my home page.
// If the caller is admin it returns error.
func Follow() error {
caller := std.PreviousRealm().Address()

if caller == config.OwnableMain.Owner() {
return ufmt.Errorf("you cannot follow yourself")
}
if profile.Followers.Add(caller) {
return nil
}
return ufmt.Errorf("you are already following")

}

// Unfollow allows a user to unfollow my home page.
func Unfollow() error {
caller := std.PreviousRealm().Address()

if profile.Followers.Remove(caller) {
return nil
}
return ufmt.Errorf("you are not following")
}

// renderRecipes renders the list of recipes.
func renderRecipes(res *mux.ResponseWriter, req *mux.Request) {
var out string
out += Header()
out += "## World Kitchen\n\n------\n\n"

// Link to margarita pizza recipe
out += "### Available Recipes:\n\n"
out += "* " + md.Link(margheritaPizza.Name, rec+"margheritaPizza") + "By : " + string(margheritaPizza.Author) + "\n"

// The list of all other recipes with clickable links
if recipes.Size() > 0 {
recipes.Iterate("", "", func(key string, value interface{}) bool {
recipe := value.(*Recipe)
out += "* " + md.Link(recipe.Name, rec+recipe.Name) + " By : " + recipe.Author.String() + "\n"
return false // continue iterating
})
out += "\n------\n\n"
} else {
out += "\nNo additional recipes yet. Be the first to add one!\n"
}
res.Write(out)
}

// renderRecipe renders the recipe details.
func renderRecipe(res *mux.ResponseWriter, req *mux.Request) {
name := req.GetVar("name")
if name == "margheritaPizza" {
res.Write(margheritaPizza.Render())
return
}
value, exists := recipes.Get(name)
if !exists {
res.Write("Recipe not found")
return
}
recipe := value.(*Recipe)
res.Write(recipe.Render())
}

func (r Recipe) Render() string {
var out string
out += Header()
out += md.H2(r.Name)
out += md.Bold("Author:") + "\n" + r.Author.String() + "\n\n"
out += md.Bold("Origin:") + "\n" + r.Origin + "\n\n"
out += md.Bold("Ingredients:") + "\n" + md.BulletList(strings.Split(r.Ingredients, "\n")) + "\n\n"
out += md.Bold("Instructions:") + "\n" + md.OrderedList(strings.Split(r.Instructions, "\n")) + "\n\n"
if r.Tips != "" {
out += md.Italic("💡 Tips:"+"\n"+r.Tips) + "\n\n"
}
out += md.HorizontalRule() + "\n"
return out
}

func renderHomepage(res *mux.ResponseWriter, req *mux.Request) {
var out string
out += Header()
out += profile.Render()
res.Write(out)
}

func (p Profile) Render() string {
var out string
out += md.H1("Welcome to my Homepage") + "\n\n" + md.HorizontalRule() + "\n\n"
out += "```\n"
out += gnoArt
out += "```\n------"
out += md.HorizontalRule() + "\n\n" + md.H2("About Me") + "\n\n"
out += md.Image("avatar", p.Avatar) + "\n\n"
out += p.AboutMe + "\n\n" + md.HorizontalRule() + "\n\n"
out += md.H3("Contact") + "\n\n"
out += md.BulletList([]string{
"Email: " + p.Email,
"GitHub: " + md.Link("@mous1985", p.Github),
"LinkedIn: " + md.Link("Mustapha", p.LinkedIn),
})
out += "\n\n" + md.Bold("👤 Followers: ") + strconv.Itoa(p.Followers.Size())
return out
}

func Header() string {
navItems := []string{
md.Link("Home", realmURL),
md.Link("World Kitchen", rec),
md.Link("Hackerspace", "https://github.com/gnolang/hackerspace/issues/86#issuecomment-2535795751"),
}
return strings.Join(navItems, " | ") + "\n\n" + md.HorizontalRule() + "\n\n"
}

func Render(path string) string {
return router.Render(path)
}
Loading

0 comments on commit 9cd5c0e

Please sign in to comment.