/ cyber-acid.go
cyber-acid.go
   1  package main
   2  
   3  import (
   4  	"encoding/json"
   5  	"log"
   6  	"net/http"
   7  	"sort"
   8  	"strconv"
   9  	"time"
  10  
  11  	"github.com/NYTimes/gziphandler"
  12  	"github.com/foolin/mixer"
  13  	"github.com/maxence-charriere/go-app/v10/pkg/app"
  14  	"github.com/mitchellh/mapstructure"
  15  	shell "github.com/stateless-minds/go-ipfs-api"
  16  )
  17  
  18  const dbNameIssue = "issue"
  19  
  20  const dbNameCitizenReputation = "citizen_reputation"
  21  
  22  const typeShortage = "shortage"
  23  
  24  const (
  25  	topicCritical = "critical"
  26  	topicIssue    = "issue"
  27  )
  28  
  29  const (
  30  	NotificationSuccess NotificationStatus = "positive"
  31  	NotificationInfo    NotificationStatus = "info"
  32  	NotificationWarning NotificationStatus = "warning"
  33  	NotificationDanger  NotificationStatus = "negative"
  34  	SuccessHeader                          = "Success"
  35  	ErrorHeader                            = "Error"
  36  )
  37  
  38  const (
  39  	asideTitleCreate = "Suggest Solution"
  40  	asideTitleList   = "List Solutions"
  41  )
  42  
  43  // pubsub is a component that does a simple pubsub on ipfs. A component is a
  44  // customizable, independent, and reusable UI element. It is created by
  45  // embedding app.Compo into a struct.
  46  type acid struct {
  47  	app.Compo
  48  	sh                         *shell.Shell
  49  	sub                        *shell.PubSubSubscription
  50  	citizenID                  string
  51  	issues                     []Issue
  52  	categoryIssues             map[string][]Issue
  53  	ranks                      []CitizenReputation
  54  	delegates                  []Delegate
  55  	currentIssueInSlice        int
  56  	Solutions                  []Solution
  57  	currentSolutionDescription string
  58  	notifications              map[string]notification
  59  	notificationID             int
  60  	AsideTitle                 string
  61  }
  62  
  63  type NotificationStatus string
  64  
  65  type notification struct {
  66  	id      int
  67  	status  string
  68  	header  string
  69  	message string
  70  }
  71  
  72  type Issue struct {
  73  	ID          string     `mapstructure:"_id" json:"_id" validate:"uuid_rfc4122"`
  74  	Type        string     `mapstructure:"type" json:"type" validate:"uuid_rfc4122"`
  75  	Category    string     `mapstructure:"category" json:"category" validate:"uuid_rfc4122"`
  76  	Description string     `mapstructure:"description" json:"description" validate:"uuid_rfc4122"`
  77  	Delegates   []Delegate `mapstructure:"delegates" json:"delegates" validate:"uuid_rfc4122"`
  78  	Solutions   []Solution `mapstructure:"solutions" json:"solutions" validate:"uuid_rfc4122"`
  79  	Voters      []string   `mapstructure:"voters" json:"voters" validate:"uuid_rfc4122"`
  80  }
  81  
  82  type Solution struct {
  83  	ID          string `mapstructure:"_id" json:"_id" validate:"uuid_rfc4122"`
  84  	Description string `mapstructure:"description" json:"description" validate:"uuid_rfc4122"`
  85  	Votes       int    `mapstructure:"votes" json:"votes" validate:"uuid_rfc4122"`
  86  }
  87  
  88  type Delegate struct {
  89  	CitizenID string `mapstructure:"citizenId" json:"citizenId" validate:"uuid_rfc4122"`
  90  	Votes     int    `mapstructure:"votes" json:"votes" validate:"uuid_rfc4122"`
  91  	Selected  int    `mapstructure:"selected" json:"selected" validate:"uuid_rfc4122"`
  92  	OwnVote   bool   `mapstructure:"voted" json:"voted" validate:"uuid_rfc4122"`
  93  }
  94  
  95  type CitizenReputation struct {
  96  	ID              string  `mapstructure:"_id" json:"_id" validate:"uuid_rfc4122"`
  97  	Type            string  `mapstructure:"type" json:"type" validate:"uuid_rfc4122"`
  98  	CitizenID       string  `mapstructure:"citizenId" json:"citizenId" validate:"uuid_rfc4122"`
  99  	ReputationIndex float64 `mapstructure:"reputationIndex" json:"reputationIndex" validate:"uuid_rfc4122"`
 100  }
 101  
 102  func (a *acid) OnMount(ctx app.Context) {
 103  	sh := shell.NewShell("localhost:5001")
 104  	a.sh = sh
 105  	myPeer, err := a.sh.ID()
 106  	if err != nil {
 107  		log.Fatal(err)
 108  	}
 109  
 110  	citizenID := myPeer.ID[len(myPeer.ID)-8:]
 111  	// replace password with your own
 112  	password := "mysecretpassword"
 113  
 114  	a.citizenID = mixer.EncodeString(password, citizenID)
 115  	a.subscribeToCriticalTopic(ctx)
 116  	a.subscribeToIssueTopic(ctx)
 117  	a.notifications = make(map[string]notification)
 118  	a.categoryIssues = make(map[string][]Issue)
 119  	ctx.Async(func() {
 120  		// err := a.sh.OrbitDocsDelete(dbNameIssue, "all")
 121  		// if err != nil {
 122  		// 	log.Fatal(err)
 123  		// }
 124  
 125  		// err := a.sh.OrbitDocsDelete(dbNameCitizenReputation, "4")
 126  		// if err != nil {
 127  		// 	log.Fatal(err)
 128  		// }
 129  
 130  		cr, err := a.sh.OrbitDocsQuery(dbNameCitizenReputation, "type", "reputation")
 131  		if err != nil {
 132  			log.Fatal(err)
 133  		}
 134  
 135  		var cc []interface{}
 136  		err = json.Unmarshal(cr, &cc)
 137  		if err != nil {
 138  			log.Fatal(err)
 139  		}
 140  
 141  		for _, zz := range cc {
 142  			r := CitizenReputation{}
 143  			err = mapstructure.Decode(zz, &r)
 144  			if err != nil {
 145  				log.Fatal(err)
 146  			}
 147  			ctx.Dispatch(func(ctx app.Context) {
 148  				a.ranks = append(a.ranks, r)
 149  				sort.SliceStable(a.ranks, func(i, j int) bool {
 150  					return a.ranks[i].ID < a.ranks[j].ID
 151  				})
 152  			})
 153  		}
 154  
 155  		v, err := a.sh.OrbitDocsQuery(dbNameIssue, "type", "shortage")
 156  		if err != nil {
 157  			log.Fatal(err)
 158  		}
 159  
 160  		var vv []interface{}
 161  		err = json.Unmarshal(v, &vv)
 162  		if err != nil {
 163  			log.Fatal(err)
 164  		}
 165  
 166  		for _, ii := range vv {
 167  			i := Issue{}
 168  			err = mapstructure.Decode(ii, &i)
 169  			if err != nil {
 170  				log.Fatal(err)
 171  			}
 172  			ctx.Dispatch(func(ctx app.Context) {
 173  				a.categoryIssues[i.Category] = append(a.categoryIssues[i.Category], i)
 174  				a.issues = append(a.issues, i)
 175  				sort.SliceStable(a.issues, func(i, j int) bool {
 176  					return a.issues[i].ID < a.issues[j].ID
 177  				})
 178  			})
 179  		}
 180  	})
 181  }
 182  
 183  func (a *acid) subscribeToCriticalTopic(ctx app.Context) {
 184  	ctx.Async(func() {
 185  		topic := topicCritical
 186  		subscription, err := a.sh.PubSubSubscribe(topic)
 187  		if err != nil {
 188  			log.Fatal(err)
 189  		}
 190  		a.sub = subscription
 191  		a.subscriptionCritical(ctx)
 192  	})
 193  }
 194  
 195  func (a *acid) subscribeToIssueTopic(ctx app.Context) {
 196  	ctx.Async(func() {
 197  		topic := topicIssue
 198  		subscription, err := a.sh.PubSubSubscribe(topic)
 199  		if err != nil {
 200  			log.Fatal(err)
 201  		}
 202  		a.sub = subscription
 203  		a.subscriptionIssue(ctx)
 204  	})
 205  }
 206  
 207  func (a *acid) subscriptionIssue(ctx app.Context) {
 208  	ctx.Async(func() {
 209  		defer a.sub.Cancel()
 210  		// wait on pubsub
 211  		res, err := a.sub.Next()
 212  		if err != nil {
 213  			log.Fatal(err)
 214  		}
 215  		// Decode the string data.
 216  		str := string(res.Data)
 217  		log.Println("Subscriber of topic issue received message: " + str)
 218  		ctx.Async(func() {
 219  			a.subscribeToIssueTopic(ctx)
 220  		})
 221  
 222  		s := Issue{}
 223  		err = json.Unmarshal([]byte(str), &s)
 224  		if err != nil {
 225  			log.Fatal(err)
 226  		}
 227  
 228  		id, err := strconv.Atoi(s.ID)
 229  		if err != nil {
 230  			log.Fatal(err)
 231  		}
 232  
 233  		ctx.Dispatch(func(ctx app.Context) {
 234  			a.issues[id-1] = s
 235  		})
 236  	})
 237  }
 238  
 239  func (a *acid) subscriptionCritical(ctx app.Context) {
 240  	ctx.Async(func() {
 241  		defer a.sub.Cancel()
 242  		// wait on pubsub
 243  		res, err := a.sub.Next()
 244  		if err != nil {
 245  			log.Fatal(err)
 246  		}
 247  		// Decode the string data.
 248  		str := string(res.Data)
 249  		log.Println("Subscriber of topic critical received message: " + str)
 250  		ctx.Async(func() {
 251  			a.subscribeToCriticalTopic(ctx)
 252  		})
 253  
 254  		s := Issue{}
 255  		err = json.Unmarshal([]byte(str), &s)
 256  		if err != nil {
 257  			log.Fatal(err)
 258  		}
 259  
 260  		var lastID int
 261  		unique := true
 262  		for n, i := range a.issues {
 263  			a.categoryIssues[i.Category] = append(a.categoryIssues[i.Category], i)
 264  			if s.Description == i.Description {
 265  				unique = false
 266  			}
 267  
 268  			if n == 0 {
 269  				lastID, err = strconv.Atoi(i.ID)
 270  				if err != nil {
 271  					log.Fatal(err)
 272  				}
 273  			} else {
 274  				currentID, err := strconv.Atoi(i.ID)
 275  				if err != nil {
 276  					log.Fatal(err)
 277  				}
 278  				previousID, err := strconv.Atoi(a.issues[n-1].ID)
 279  				if err != nil {
 280  					log.Fatal(err)
 281  				}
 282  				if currentID > previousID {
 283  					lastID = currentID
 284  				}
 285  			}
 286  
 287  		}
 288  		if unique {
 289  			newID := lastID + 1
 290  			issue := Issue{
 291  				ID:          strconv.Itoa(newID),
 292  				Type:        typeShortage,
 293  				Category:    s.Category,
 294  				Description: s.Description,
 295  				Solutions:   []Solution{},
 296  			}
 297  
 298  			i, err := json.Marshal(issue)
 299  			if err != nil {
 300  				log.Fatal(err)
 301  			}
 302  
 303  			err = a.sh.OrbitDocsPut(dbNameIssue, i)
 304  			if err != nil {
 305  				log.Fatal(err)
 306  			}
 307  
 308  			err = a.sh.PubSubPublish(topicIssue, string(i))
 309  			if err != nil {
 310  				log.Fatal(err)
 311  			}
 312  
 313  			ctx.Dispatch(func(ctx app.Context) {
 314  				a.issues = append(a.issues, issue)
 315  			})
 316  		}
 317  	})
 318  }
 319  
 320  // The Render method is where the component appearance is defined. Here, a
 321  // "pubsub World!" is displayed as a heading.
 322  func (a *acid) Render() app.UI {
 323  	return app.Div().Class("l-application").Role("presentation").Body(
 324  		app.Link().Rel("stylesheet").Href("https://assets.ubuntu.com/v1/vanilla-framework-version-3.8.0.min.css"),
 325  		app.Link().Rel("stylesheet").Href("https://use.fontawesome.com/releases/v6.2.0/css/all.css"),
 326  		app.Link().Rel("stylesheet").Href("/app.css"),
 327  		app.Header().Class("l-navigation is-collapsed").Body(
 328  			app.Div().Class("l-navigation__drawer").Body(
 329  				app.Div().Class("p-panel is-dark").Body(
 330  					app.Div().Class("p-panel__header is-sticky").Body(
 331  						app.A().Class("p-panel__logo").Href("#").Body(
 332  							app.H5().Class("p-heading--2").Text("Cyber Acid"),
 333  						),
 334  					),
 335  					app.Hr(),
 336  					app.P().Class("p-heading--6").Body(
 337  						app.Text("Liquid democracy politics simulator based on the automated data feed from the moneyless economy simulator "),
 338  						app.A().Href("https://github.com/stateless-minds/cyber-stasis").Text("Cyber Stasis"),
 339  					).Style("padding", "0 10%;"),
 340  					app.Hr(),
 341  					app.Div().Class("p-panel__content").Body(
 342  						app.Div().Class("p-side-navigation--icons is-dark").ID("drawer-icons").Body(
 343  							app.Nav().Aria("label", "Main"),
 344  							app.Ul().Class("p-side-navigation__list").Body(
 345  								app.Li().Class("p-side-navigation__item--title").Body(
 346  									app.A().Class("p-side-navigation__link").Href("#").Body(
 347  										app.I().Class("p-icon--help is-light p-side-navigation__icon"),
 348  										app.Span().Class("p-side-navigation__label").Text("How to play"),
 349  									).OnClick(a.openHowToDialog),
 350  									app.A().Class("p-side-navigation__link").Href("#").Body(
 351  										app.I().Class("p-icon--warning is-light p-side-navigation__icon"),
 352  										app.Span().Class("p-side-navigation__label").Text("Shortages"),
 353  									).Aria("current", "page"),
 354  									app.A().Class("p-side-navigation__link").Href("#").Body(
 355  										app.I().Class("p-icon--share is-light p-side-navigation__icon"),
 356  										app.Span().Class("p-side-navigation__label").Text("Delegate rankings"),
 357  									).OnClick(a.openRankingsDialog),
 358  								),
 359  							),
 360  						),
 361  					),
 362  				),
 363  			),
 364  		),
 365  		app.Main().Class("l-main").Body(
 366  			app.Div().Class("p-panel").Body(
 367  				app.If(len(a.notifications) > 0, func() app.UI {
 368  					return app.Range(a.notifications).Map(func(s string) app.UI {
 369  						return app.Div().Class("p-notification--" + a.notifications[s].status).Body(
 370  							app.Div().Class("p-notification__content").Body(
 371  								app.H5().Class("p-notification__title").Text(a.notifications[s].header),
 372  								app.P().Class("p-notification__message").Text(a.notifications[s].message),
 373  							),
 374  						)
 375  					})
 376  				}),
 377  				app.Div().Class("p-panel__header").Body(
 378  					app.H4().Class("p-panel__title").Text("Open Issues"),
 379  				),
 380  				app.Div().Class("p-panel__content").Body(
 381  					app.Div().Class("u-fixed-width").Body(
 382  						app.If(len(a.categoryIssues) > 0, func() app.UI {
 383  							return app.Range(a.categoryIssues).Map(func(s string) app.UI {
 384  								return app.Table().Aria("label", "Issues table").Class("p-main-table").Body(
 385  									app.THead().Body(
 386  										app.Tr().Body(
 387  											app.Th().Body(
 388  												app.Span().Class("status-icon is-running").Text("Category "+s),
 389  											),
 390  											app.Th().Text("Actions"),
 391  										),
 392  									),
 393  									app.If(len(a.categoryIssues[s]) > 0, func() app.UI {
 394  										return app.TBody().Body(
 395  											app.Range(a.categoryIssues[s]).Slice(func(i int) app.UI {
 396  												return app.Tr().DataSet("id", i).Body(
 397  													app.Td().DataSet("column", "issue").Body(
 398  														app.Div().Text(a.categoryIssues[s][i].Description),
 399  													),
 400  													app.Td().DataSet("column", "action").Body(
 401  														app.Div().Body(
 402  															app.Button().Class("u-no-margin--bottom").Text("List Solutions").Value(a.categoryIssues[s][i].ID).OnMouseOver(a.asidePreloadList).OnClick(a.asideOpenList),
 403  															app.Button().Class("u-no-margin--bottom").Text("Suggest Solution").Value(a.categoryIssues[s][i].ID).OnMouseOver(a.asidePreloadCreate).OnClick(a.asideOpenCreate),
 404  														),
 405  													),
 406  												)
 407  											}),
 408  										)
 409  									}),
 410  								)
 411  							})
 412  						}),
 413  					),
 414  					app.Div().Class("p-modal").ID("howto-modal").Style("display", "none").Body(
 415  						app.Section().Class("p-modal__dialog").Role("dialog").Aria("modal", true).Aria("labelledby", "modal-title").Aria("describedby", "modal-description").Body(
 416  							app.Header().Class("p-modal__header").Body(
 417  								app.H2().Class("p-modal__title").ID("modal-title").Text("How to play"),
 418  								app.Button().Class("p-modal__close").Aria("label", "Close active modal").Aria("controls", "modal").OnClick(a.closeHowToModal),
 419  							),
 420  							app.Div().Class("p-heading-icon--small").Body(
 421  								app.Aside().Class("p-accordion").Body(
 422  									app.Ul().Class("p-accordion__list").Body(
 423  										app.Li().Class("p-accordion__group").Body(
 424  											app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body(
 425  												app.Button().Type("button").Class("p-accordion__tab").ID("tab1").Aria("controls", "tab1-section").Aria("expanded", true).Text("What is Cyber Acid").Value("tab1-section").OnClick(a.toggleAccordion),
 426  											),
 427  											app.Section().Class("p-accordion__panel").ID("tab1-section").Aria("hidden", false).Aria("labelledby", "tab1").Body(
 428  												app.P().Text("Cyber Acid is a political simulator based on the liquid democracy concept. It is designed as an integration module that works with Cyber Stasis - the moneyless economy simulator."),
 429  											),
 430  										),
 431  										app.Li().Class("p-accordion__group").Body(
 432  											app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body(
 433  												app.Button().Type("button").Class("p-accordion__tab").ID("tab2").Aria("controls", "tab2-section").Aria("expanded", true).Text("What is liquid democracy").Value("tab2-section").OnClick(a.toggleAccordion),
 434  											),
 435  											app.Section().Class("p-accordion__panel").ID("tab2-section").Aria("hidden", true).Aria("labelledby", "tab1").Body(
 436  												app.P().Text("Liquid democracy meets the transparency and accountability of direct democracy with the easy of use of representative democracy. Vote directly for what you want and delegate one-time voting rights per topic."),
 437  											),
 438  										),
 439  										app.Li().Class("p-accordion__group").Body(
 440  											app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body(
 441  												app.Button().Type("button").Class("p-accordion__tab").ID("tab3").Aria("controls", "tab3-section").Aria("expanded", true).Text("How it works").Value("tab3-section").OnClick(a.toggleAccordion),
 442  											),
 443  											app.Section().Class("p-accordion__panel").ID("tab3-section").Aria("hidden", true).Aria("labelledby", "tab3").Body(
 444  												app.P().Text("The simulator receives live data from Cyber Stasis about critical shortages of production and resources. The goal of all participants is to suggest solutions to those issues. For example - replacing a resource with another one, researching new technologies etc."),
 445  											),
 446  										),
 447  										app.Li().Class("p-accordion__group").Body(
 448  											app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body(
 449  												app.Button().Type("button").Class("p-accordion__tab").ID("tab4").Aria("controls", "tab4-section").Aria("expanded", true).Text("Features").Value("tab4-section").OnClick(a.toggleAccordion),
 450  											),
 451  											app.Section().Class("p-accordion__panel").ID("tab4-section").Aria("hidden", true).Aria("labelledby", "tab3").Body(
 452  												app.Ul().Class("p-matrix").Body(
 453  													app.Li().Class("p-matrix__item").Body(
 454  														app.Div().Class("p-matrix__content").Body(
 455  															app.H3().Class("p-matrix__title").Text("Check shortages"),
 456  															app.Div().Class("p-matrix__desc").Body(
 457  																app.P().Text("Review pressing issues."),
 458  															),
 459  														),
 460  													),
 461  													app.Li().Class("p-matrix__item").Body(
 462  														app.Div().Class("p-matrix__content").Body(
 463  															app.H3().Class("p-matrix__title").Text("Suggest a solution"),
 464  															app.Div().Class("p-matrix__desc").Body(
 465  																app.P().Text("Contribute with your expertise."),
 466  															),
 467  														),
 468  													),
 469  													app.Li().Class("p-matrix__item").Body(
 470  														app.Div().Class("p-matrix__content").Body(
 471  															app.H3().Class("p-matrix__title").Text("Vote for solutions"),
 472  															app.Div().Class("p-matrix__desc").Body(
 473  																app.P().Text("Vote for the best solution."),
 474  															),
 475  														),
 476  													),
 477  													app.Li().Class("p-matrix__item").Body(
 478  														app.Div().Class("p-matrix__content").Body(
 479  															app.H3().Class("p-matrix__title").Text("Delegate your vote"),
 480  															app.Div().Class("p-matrix__desc").Body(
 481  																app.P().Text("Not competent? Delegate your vote."),
 482  															),
 483  														),
 484  													),
 485  													app.Li().Class("p-matrix__item").Body(
 486  														app.Div().Class("p-matrix__content").Body(
 487  															app.H3().Class("p-matrix__title").Text("Infinite delegation"),
 488  															app.Div().Class("p-matrix__desc").Body(
 489  																app.P().Text("Delegation can be chained for maximum participation."),
 490  															),
 491  														),
 492  													),
 493  													app.Li().Class("p-matrix__item").Body(
 494  														app.Div().Class("p-matrix__content").Body(
 495  															app.H3().Class("p-matrix__title").Text("Cross delegation"),
 496  															app.Div().Class("p-matrix__desc").Body(
 497  																app.P().Text("Cross delegation is also supported."),
 498  															),
 499  														),
 500  													),
 501  												),
 502  											),
 503  										),
 504  										app.Li().Class("p-accordion__group").Body(
 505  											app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body(
 506  												app.Button().Type("button").Class("p-accordion__tab").ID("tab5").Aria("controls", "tab5-section").Aria("expanded", true).Text("Support us").Value("tab5-section").OnClick(a.toggleAccordion),
 507  											),
 508  											app.Section().Class("p-accordion__panel").ID("tab5-section").Aria("hidden", true).Aria("labelledby", "tab5").Body(
 509  												app.A().Href("https://opencollective.com/stateless-minds-collective").Text("https://opencollective.com/stateless-minds-collective"),
 510  											),
 511  										),
 512  										app.Li().Class("p-accordion__group").Body(
 513  											app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body(
 514  												app.Button().Type("button").Class("p-accordion__tab").ID("tab6").Aria("controls", "tab6-section").Aria("expanded", true).Text("Terms of service").Value("tab6-section").OnClick(a.toggleAccordion),
 515  											),
 516  											app.Section().Class("p-accordion__panel").ID("tab6-section").Aria("hidden", true).Aria("labelledby", "tab6").Body(
 517  												app.Div().Class("p-card").Body(
 518  													app.H3().Text("Introduction"),
 519  													app.P().Class("p-card__content").Text("Cyber Acid is a liquid democracy political simulator in the form of a fictional game based on real-time data from Cyber Stasis. By using the application you are implicitly agreeing to share your peer id with the IPFS public network."),
 520  												),
 521  												app.Div().Class("p-card").Body(
 522  													app.H3().Text("Application Hosting"),
 523  													app.P().Class("p-card__content").Text("Cyber Acid is a decentralized application and is hosted on a public peer to peer network. By using the application you agree to host it on the public IPFS network free of charge for as long as your usage is."),
 524  												),
 525  												app.Div().Class("p-card").Body(
 526  													app.H3().Text("User-Generated Content"),
 527  													app.P().Class("p-card__content").Text("All published content is user-generated, fictional and creators are not responsible for it."),
 528  												),
 529  											),
 530  										),
 531  										app.Li().Class("p-accordion__group").Body(
 532  											app.Div().Role("heading").Aria("level", "3").Class("p-accordion__heading").Body(
 533  												app.Button().Type("button").Class("p-accordion__tab").ID("tab7").Aria("controls", "tab7-section").Aria("expanded", true).Text("Privacy policy").Value("tab7-section").OnClick(a.toggleAccordion),
 534  											),
 535  											app.Section().Class("p-accordion__panel").ID("tab7-section").Aria("hidden", true).Aria("labelledby", "tab7").Body(
 536  												app.Div().Class("p-card").Body(
 537  													app.H3().Text("Personal data"),
 538  													app.P().Class("p-card__content").Text("There is no personal information collected within Cyber Acid. We store a small portion of your peer ID encrypted as a non-unique identifier which is used for displaying the ranks interface."),
 539  												),
 540  												app.Div().Class("p-card").Body(
 541  													app.H3().Text("Coookies"),
 542  													app.P().Class("p-card__content").Text("Cyber Acid does not use cookies."),
 543  												),
 544  												app.Div().Class("p-card").Body(
 545  													app.H3().Text("Links to Cyber Stasis"),
 546  													app.P().Class("p-card__content").Text("Cyber Acid contains links to its sister project Cyber Stasis and depends on its data to function properly."),
 547  												),
 548  												app.Div().Class("p-card").Body(
 549  													app.H3().Text("Changes to this privacy policy"),
 550  													app.P().Class("p-card__content").Text("This Privacy Policy might be updated from time to time. Thus, it is advised to review this page periodically for any changes. You will be notified of any changes from this page. Changes are effective immediately after they are posted on this page."),
 551  												),
 552  											),
 553  										),
 554  									),
 555  								),
 556  							),
 557  						).Style("left", "10%").Style("width", "80%"),
 558  					),
 559  					app.Div().Class("p-modal").ID("rankings-modal").Style("display", "none").Body(
 560  						app.Section().Class("p-modal__dialog").Role("dialog").Aria("modal", true).Aria("labelledby", "modal-title").Aria("describedby", "modal-description").Body(
 561  							app.Header().Class("p-modal__header").Body(
 562  								app.H2().Class("p-modal__title").ID("modal-title").Text("Delegate rankings"),
 563  								app.Button().Class("p-modal__close").Aria("label", "Close active modal").Aria("controls", "modal").OnClick(a.closeRankingsModal),
 564  							),
 565  							app.Table().Aria("label", "Rankings table").Class("p-main-table").Body(
 566  								app.THead().Body(
 567  									app.Tr().Body(
 568  										app.Th().Body(
 569  											app.Span().Class("status-icon is-blocked").Text("Delegate ID"),
 570  										),
 571  										app.Th().Text("Trust"),
 572  									),
 573  								),
 574  								app.If(len(a.delegates) > 0, func() app.UI {
 575  									return app.TBody().Body(
 576  									app.Range(a.delegates).Slice(func(i int) app.UI {
 577  										return app.Tr().DataSet("id", i).Body(
 578  											app.Td().DataSet("column", "delegate").Body(
 579  												app.Div().Text(a.delegates[i].CitizenID),
 580  											),
 581  											app.Td().DataSet("column", "trust").Body(
 582  												app.Div().Text(a.delegates[i].Selected),
 583  											),
 584  										)
 585  									}),
 586  									)
 587  								}),
 588  							),
 589  						).Style("left", "10%").Style("width", "80%"),
 590  					),
 591  				),
 592  			),
 593  		),
 594  		app.Aside().Class("l-aside is-collapsed").ID("aside-panel").Body(
 595  			app.Div().Class("p-panel").Body(
 596  				app.Div().Class("p-panel__header").Body(
 597  					app.H4().Class("p-panel__title").Text(a.AsideTitle),
 598  					app.Div().Class("p-panel__controls").Body(
 599  						app.Button().Class("p-button--base u-no-margin--bottom has-icon").Body(app.I().Class("p-icon--close")).OnClick(a.asideClose),
 600  					),
 601  				),
 602  				app.If(a.AsideTitle == asideTitleCreate, func() app.UI {
 603  					return app.Div().Class("p-panel__content").Body(
 604  						app.Div().Class("p-form p-form--stacked").Body(
 605  							app.Div().Class("p-form__group row").Body(
 606  								app.Textarea().ID("solution").Name("solution").Rows(3).OnKeyUp(a.onSolution),
 607  							),
 608  						),
 609  						app.Div().Class("row").Body(
 610  							app.Div().Class("col-12").Body(
 611  								app.Button().Class("p-button--positive u-float-right").Name("submit-solution").Text("Submit Solution").OnClick(a.submitSolution),
 612  							),
 613  						),
 614  					)
 615  				}).ElseIf(a.AsideTitle == asideTitleList, func() app.UI {
 616  					return app.Div().Class("p-panel__content").Body(
 617  						app.Ul().Class("p-list-tree").Aria("multiselectable", true).Role("tree").Body(
 618  							app.Li().Class("p-list-tree__item p-list-tree__item--group").Role("treeitem").Body(
 619  								app.Button().Class("p-list-tree__toggle").ID("sub-1-btn").Aria("controls", "sub-1").Aria("expanded", true).Text("Suggested Solutions"),
 620  								app.Ul().Class("p-list-tree").Role("group").ID("sub-1").Aria("hidden", false).Aria("labelledby", "sub-1-btn").Body(
 621  									app.Range(a.Solutions).Slice(func(i int) app.UI {
 622  										return app.Li().Class("p-list-tree__item").Role("treeitem").Body(
 623  											app.P().Text(a.Solutions[i].Description),
 624  											app.If(len(a.issues[a.currentIssueInSlice].Voters) > 0, func() app.UI {
 625  												return app.If(sliceContains(a.issues[a.currentIssueInSlice].Voters, a.citizenID), func() app.UI {
 626  													return app.Button().
 627  													Class("p-button is-small is-inline").
 628  													Text("Vote").
 629  													Value(a.Solutions[i].ID).
 630  													OnClick(a.vote).
 631  													Disabled(true).
 632  													Body(
 633  														app.I().Class("fa-solid fa-thumbs-up"), // Icon
 634  														app.Span().
 635  															Class("p-badge").
 636  															Aria("label", strconv.Itoa(a.Solutions[i].Votes)+" votes").
 637  															Text(strconv.Itoa(a.Solutions[i].Votes)), // Votes count
 638  													)												
 639  												}).Else( func() app.UI {
 640  													return app.Div().Class("vote-delegate-container").Body(
 641  														// Vote Button with Icon
 642  														app.Button().
 643  															Class("p-button is-small is-inline").
 644  															Text("Vote").
 645  															Value(a.Solutions[i].ID).
 646  															OnClick(a.vote).
 647  															Body(
 648  																app.I().Class("fa-regular fa-thumbs-up"), // Icon for Vote
 649  															),
 650  													
 651  														// Delegate Button
 652  														app.Button().
 653  															Class("p-button is-small is-inline").
 654  															ID("show-modal").
 655  															Text("Delegate...").
 656  															Aria("controls", "modal").
 657  															Value(a.Solutions[i].ID).
 658  															OnClick(a.openDelegateDialog),
 659  													
 660  														// Modal for Delegation
 661  														app.Div().
 662  															Class("p-modal").
 663  															ID("delegate-modal").
 664  															Style("display", "none"). // Initially hidden
 665  															Body(
 666  																app.Section().
 667  																	Class("p-modal__dialog").
 668  																	Role("dialog").
 669  																	Aria("modal", true).
 670  																	Aria("labelledby", "modal-title").
 671  																	Aria("describedby", "modal-description").
 672  																	Body(
 673  																		app.Header().
 674  																			Class("p-modal__header").
 675  																			Body(
 676  																				app.H2().
 677  																					Class("p-modal__title").
 678  																					ID("modal-title").
 679  																					Text("Delegate"),
 680  																				app.Button().
 681  																					Class("p-modal__close").
 682  																					Aria("label", "Close active modal").
 683  																					Aria("controls", "modal").
 684  																					Value(a.Solutions[i].ID).
 685  																					OnClick(a.closeDelegateModal),
 686  																			),
 687  																		app.P().
 688  																			ID("modal-description").
 689  																			Text("Select a citizen to represent your vote for this issue:"),
 690  																		app.Div().
 691  																			Class("p-heading-icon--small").
 692  																			Body(
 693  																				app.Range(a.ranks).Slice(func(i int) app.UI {
 694  																					return app.If(a.ranks[i].CitizenID != a.citizenID, func() app.UI {
 695  																						return app.Div().Class("p-heading-icon__header").Body(
 696  																							app.Button().
 697  																								Class("p-chip").
 698  																								Aria("pressed", true).
 699  																								Disabled(true).
 700  																								Body(
 701  																									app.Span().Class("p-chip__value").Text("Citizen"),
 702  																									app.Span().Class("p-badge").Aria("label", "Citizen").Text(a.ranks[i].CitizenID),
 703  																								),
 704  																							app.Button().
 705  																								Class("p-chip").
 706  																								Aria("pressed", true).
 707  																								Disabled(true).
 708  																								Body(
 709  																									app.Span().Class("p-chip__value").Text("Reputation"),
 710  																									app.Span().Class("p-badge").Aria("label", "Reputation").Text(a.ranks[i].ReputationIndex),
 711  																								),
 712  																							app.Button().
 713  																								Class("p-chip").
 714  																								Body(
 715  																									app.Span().Class("p-chip__value").Text("Select"),
 716  																								).Value(a.ranks[i].CitizenID).OnClick(a.delegate),
 717  																						)
 718  																					})
 719  																				}),
 720  																			),
 721  																	),
 722  														),
 723  													)													
 724  												})
 725  											}).Else( func() app.UI {
 726  												return app.Div().Class("vote-delegate-container").Body(
 727  													// Vote Button with Icon
 728  													app.Button().
 729  														Class("p-button is-small is-inline").
 730  														Text("Vote").
 731  														Value(a.Solutions[i].ID).
 732  														OnClick(a.vote).
 733  														Body(
 734  															app.I().Class("fa-regular fa-thumbs-up"), // Icon for Vote
 735  														),
 736  												
 737  													// Delegate Button
 738  													app.Button().
 739  														Class("p-button is-small is-inline").
 740  														ID("show-modal").
 741  														Text("Delegate...").
 742  														Aria("controls", "modal").
 743  														Value(a.Solutions[i].ID).
 744  														OnClick(a.openDelegateDialog),
 745  												
 746  													// Modal for Delegation
 747  													app.Div().
 748  														Class("p-modal").
 749  														ID("delegate-modal").
 750  														Style("display", "none"). // Initially hidden
 751  														Body(
 752  															app.Section().
 753  																Class("p-modal__dialog").
 754  																Role("dialog").
 755  																Aria("modal", true).
 756  																Aria("labelledby", "modal-title").
 757  																Aria("describedby", "modal-description").
 758  																Body(
 759  																	app.Header().
 760  																		Class("p-modal__header").
 761  																		Body(
 762  																			app.H2().
 763  																				Class("p-modal__title").
 764  																				ID("modal-title").
 765  																				Text("Delegate"),
 766  																			app.Button().
 767  																				Class("p-modal__close").
 768  																				Aria("label", "Close active modal").
 769  																				Aria("controls", "modal").
 770  																				Value(a.Solutions[i].ID).
 771  																				OnClick(a.closeDelegateModal),
 772  																		),
 773  																	app.P().
 774  																		ID("modal-description").
 775  																		Text("Select a citizen to represent your vote for this issue:"),
 776  																	app.Div().
 777  																		Class("p-heading-icon--small").
 778  																		Body(
 779  																			app.Range(a.ranks).Slice(func(i int) app.UI {
 780  																				return app.If(a.ranks[i].CitizenID != a.citizenID, func() app.UI {
 781  																					return app.Div().Class("p-heading-icon__header").Body(
 782  																						app.Button().
 783  																							Class("p-chip").
 784  																							Aria("pressed", true).
 785  																							Disabled(true).
 786  																							Body(
 787  																								app.Span().Class("p-chip__value").Text("Citizen"),
 788  																								app.Span().Class("p-badge").Aria("label", "Citizen").Text(a.ranks[i].CitizenID),
 789  																							),
 790  																						app.Button().
 791  																							Class("p-chip").
 792  																							Aria("pressed", true).
 793  																							Disabled(true).
 794  																							Body(
 795  																								app.Span().Class("p-chip__value").Text("Reputation"),
 796  																								app.Span().Class("p-badge").Aria("label", "Reputation").Text(a.ranks[i].ReputationIndex),
 797  																							),
 798  																						app.Button().
 799  																							Class("p-chip").
 800  																							Body(
 801  																								app.Span().Class("p-chip__value").Text("Select"),
 802  																							).Value(a.ranks[i].CitizenID).OnClick(a.delegate),
 803  																					)
 804  																				})
 805  																			}),
 806  																		),
 807  																),
 808  														),
 809  												)												
 810  											}),
 811  										)
 812  									}),
 813  								),
 814  							),
 815  						),
 816  					)
 817  				}),
 818  			),
 819  		),
 820  	)
 821  }
 822  
 823  func (a *acid) asidePreloadList(ctx app.Context, e app.Event) {
 824  	issueID := ctx.JSSrc().Get("value").String()
 825  	issueIDInt, err := strconv.Atoi(issueID)
 826  	if err != nil {
 827  		log.Fatal(err)
 828  	}
 829  	a.currentIssueInSlice = issueIDInt - 1
 830  	a.Solutions = a.issues[issueIDInt-1].Solutions
 831  	a.AsideTitle = asideTitleList
 832  }
 833  
 834  func (a *acid) asideOpenList(ctx app.Context, e app.Event) {
 835  	app.Window().Get("document").Call("querySelector", ".l-aside").Get("classList").Call("remove", "is-collapsed")
 836  }
 837  
 838  func (a *acid) asidePreloadCreate(ctx app.Context, e app.Event) {
 839  	a.AsideTitle = asideTitleCreate
 840  }
 841  
 842  func (a *acid) asideOpenCreate(ctx app.Context, e app.Event) {
 843  	app.Window().Get("document").Call("querySelector", ".l-aside").Get("classList").Call("remove", "is-collapsed")
 844  	app.Window().Get("document").Call("querySelector", ".p-button--positive").Call("setAttribute", "id", ctx.JSSrc().Get("value").String())
 845  }
 846  
 847  func (a *acid) asideClose(ctx app.Context, e app.Event) {
 848  	app.Window().Get("document").Call("querySelector", ".l-aside").Get("classList").Call("add", "is-collapsed")
 849  }
 850  
 851  func (a *acid) onSolution(ctx app.Context, e app.Event) {
 852  	a.currentSolutionDescription = ctx.JSSrc().Get("value").String()
 853  }
 854  
 855  func (a *acid) vote(ctx app.Context, e app.Event) {
 856  	ctx.JSSrc().Get("firstChild").Get("classList").Call("remove", "fa-regular")
 857  
 858  	val := ctx.JSSrc().Get("value").String()
 859  	solutionID, err := strconv.Atoi(val)
 860  	if err != nil {
 861  		log.Fatal(err)
 862  	}
 863  
 864  	currentIssue := a.issues[a.currentIssueInSlice]
 865  	var delegate bool
 866  	var delegatedVotes int
 867  	var ownVote bool
 868  	// delegated voting logic
 869  	for i, d := range currentIssue.Delegates {
 870  		if a.citizenID == d.CitizenID {
 871  			delegate = true
 872  			ownVote = currentIssue.Delegates[i].OwnVote
 873  			if !d.OwnVote {
 874  				currentIssue.Delegates[i].OwnVote = true
 875  
 876  			}
 877  			delegatedVotes = d.Votes
 878  			currentIssue.Delegates[i].Votes = 0
 879  		}
 880  	}
 881  
 882  	currentIssue.Voters = append(currentIssue.Voters, a.citizenID)
 883  
 884  	if delegate {
 885  		if !ownVote {
 886  			currentIssue.Solutions[solutionID-1].Votes += delegatedVotes + 1
 887  		} else {
 888  			currentIssue.Solutions[solutionID-1].Votes += delegatedVotes
 889  		}
 890  
 891  	} else {
 892  		currentIssue.Solutions[solutionID-1].Votes++
 893  	}
 894  
 895  	i, err := json.Marshal(currentIssue)
 896  	if err != nil {
 897  		log.Fatal(err)
 898  	}
 899  	ctx.Async(func() {
 900  		err = a.sh.OrbitDocsPut(dbNameIssue, i)
 901  		if err != nil {
 902  			ctx.Dispatch(func(ctx app.Context) {
 903  				a.createNotification(ctx, NotificationDanger, ErrorHeader, "Could not vote for solution. Try again later.")
 904  				log.Fatal(err)
 905  			})
 906  		}
 907  		err = a.sh.PubSubPublish(topicIssue, string(i))
 908  		if err != nil {
 909  			log.Fatal(err)
 910  		}
 911  		ctx.Dispatch(func(ctx app.Context) {
 912  			a.issues[a.currentIssueInSlice] = currentIssue
 913  			a.createNotification(ctx, NotificationSuccess, SuccessHeader, "Vote accepted.")
 914  		})
 915  	})
 916  }
 917  
 918  func (a *acid) openRankingsDialog(ctx app.Context, e app.Event) {
 919  	for _, i := range a.issues {
 920  		a.delegates = append(a.delegates, i.Delegates...)
 921  	}
 922  	sort.SliceStable(a.delegates, func(i, j int) bool {
 923  		return a.delegates[i].Selected > a.delegates[j].Selected
 924  	})
 925  	app.Window().GetElementByID("rankings-modal").Set("style", "display:flex")
 926  }
 927  
 928  func (a *acid) openHowToDialog(ctx app.Context, e app.Event) {
 929  	app.Window().GetElementByID("howto-modal").Set("style", "display:flex")
 930  }
 931  
 932  func (a *acid) openDelegateDialog(ctx app.Context, e app.Event) {
 933  	app.Window().GetElementByID("delegate-modal").Set("style", "display:flex")
 934  }
 935  
 936  func (a *acid) delegate(ctx app.Context, e app.Event) {
 937  	citizenID := ctx.JSSrc().Get("value").String()
 938  	issue := a.issues[a.currentIssueInSlice]
 939  
 940  	var delegateExists bool
 941  	var delegate Delegate
 942  	votesTransfer := 1
 943  	if len(issue.Delegates) > 0 {
 944  		for ii, dd := range issue.Delegates {
 945  			// recursive delegation logic
 946  			if dd.CitizenID == a.citizenID {
 947  				// transfer origin votes to recipient plus own vote
 948  				votesTransfer = dd.Votes + 1
 949  				// set origin delegator's votes to zero
 950  				issue.Delegates[ii].Votes = 0
 951  				// set origin delegator as voted
 952  				issue.Delegates[ii].OwnVote = true
 953  			}
 954  		}
 955  
 956  		for i, d := range issue.Delegates {
 957  			if d.CitizenID == citizenID {
 958  				issue.Delegates[i].Votes += votesTransfer
 959  				issue.Delegates[i].Selected++
 960  				delegateExists = true
 961  			}
 962  		}
 963  	}
 964  
 965  	if len(issue.Delegates) == 0 || !delegateExists {
 966  		delegate = Delegate{
 967  			CitizenID: citizenID,
 968  			Votes:     votesTransfer,
 969  			Selected:  1,
 970  		}
 971  		issue.Delegates = append(issue.Delegates, delegate)
 972  	}
 973  	issue.Voters = append(issue.Voters, a.citizenID)
 974  	var voters []string
 975  	for _, v := range issue.Voters {
 976  		// if the delegate already voted previously remove from voters so he can vote again on new delegation
 977  		if citizenID != v {
 978  			voters = append(voters, v)
 979  		}
 980  	}
 981  	issue.Voters = voters
 982  
 983  	i, err := json.Marshal(issue)
 984  	if err != nil {
 985  		log.Fatal(err)
 986  	}
 987  
 988  	ctx.Async(func() {
 989  		err = a.sh.OrbitDocsPut(dbNameIssue, i)
 990  		if err != nil {
 991  			log.Fatal(err)
 992  		}
 993  
 994  		err = a.sh.PubSubPublish(topicIssue, string(i))
 995  		if err != nil {
 996  			log.Fatal(err)
 997  		}
 998  
 999  		ctx.Dispatch(func(ctx app.Context) {
1000  			a.issues[a.currentIssueInSlice] = issue
1001  			a.closeDelegateModal(ctx, e)
1002  			a.createNotification(ctx, NotificationSuccess, SuccessHeader, "Vote delegated.")
1003  		})
1004  	})
1005  }
1006  
1007  func (a *acid) toggleAccordion(ctx app.Context, e app.Event) {
1008  	id := ctx.JSSrc().Get("value").String()
1009  	attr := app.Window().GetElementByID(id).Get("attributes")
1010  	aria := attr.Get("aria-hidden").Get("value").String()
1011  	if aria == "false" {
1012  		app.Window().GetElementByID(id).Call("setAttribute", "aria-hidden", "true")
1013  	} else {
1014  		app.Window().GetElementByID(id).Call("setAttribute", "aria-hidden", "false")
1015  	}
1016  }
1017  
1018  func (a *acid) closeRankingsModal(ctx app.Context, e app.Event) {
1019  	app.Window().GetElementByID("rankings-modal").Set("style", "display:none")
1020  }
1021  
1022  func (a *acid) closeHowToModal(ctx app.Context, e app.Event) {
1023  	app.Window().GetElementByID("howto-modal").Set("style", "display:none")
1024  }
1025  
1026  func (a *acid) closeDelegateModal(ctx app.Context, e app.Event) {
1027  	app.Window().GetElementByID("delegate-modal").Set("style", "display:none")
1028  }
1029  
1030  func (a *acid) submitSolution(ctx app.Context, e app.Event) {
1031  	idStr := ctx.JSSrc().Get("id").String()
1032  	id, err := strconv.Atoi(idStr)
1033  	if err != nil {
1034  		log.Fatal(err)
1035  	}
1036  
1037  	lastSolutionID := 0
1038  	unique := true
1039  	if len(a.issues[id-1].Solutions) > 0 {
1040  		solutions := a.issues[id-1].Solutions
1041  		for n, s := range solutions {
1042  			if s.Description == a.currentSolutionDescription {
1043  				unique = false
1044  			}
1045  			if n > 0 {
1046  				currentID, err := strconv.Atoi(s.ID)
1047  				if err != nil {
1048  					log.Fatal(err)
1049  				}
1050  				previousID, err := strconv.Atoi(solutions[n-1].ID)
1051  				if err != nil {
1052  					log.Fatal(err)
1053  				}
1054  				if currentID > previousID {
1055  					lastSolutionID = currentID
1056  				}
1057  			} else {
1058  				lastSolutionID = 1
1059  			}
1060  		}
1061  	}
1062  
1063  	if unique {
1064  		solution := Solution{
1065  			ID:          strconv.Itoa(lastSolutionID + 1),
1066  			Description: a.currentSolutionDescription,
1067  			Votes:       0,
1068  		}
1069  
1070  		a.issues[id-1].Solutions = append(a.issues[id-1].Solutions, solution)
1071  
1072  		i, err := json.Marshal(a.issues[id-1])
1073  		if err != nil {
1074  			log.Fatal(err)
1075  		}
1076  
1077  		ctx.Async(func() {
1078  			err = a.sh.OrbitDocsPut(dbNameIssue, i)
1079  			if err != nil {
1080  				ctx.Dispatch(func(ctx app.Context) {
1081  					a.createNotification(ctx, NotificationDanger, ErrorHeader, "Could not create solution. Try again later.")
1082  					log.Fatal(err)
1083  				})
1084  			}
1085  			err = a.sh.PubSubPublish(topicIssue, string(i))
1086  			if err != nil {
1087  				log.Fatal(err)
1088  			}
1089  
1090  			ctx.Dispatch(func(ctx app.Context) {
1091  				app.Window().Get("document").Call("querySelector", ".l-aside").Get("classList").Call("add", "is-collapsed")
1092  				a.createNotification(ctx, NotificationSuccess, SuccessHeader, "Solution submited.")
1093  			})
1094  		})
1095  	}
1096  }
1097  
1098  func (a *acid) createNotification(ctx app.Context, s NotificationStatus, h string, msg string) {
1099  	a.notificationID++
1100  	a.notifications[strconv.Itoa(a.notificationID)] = notification{
1101  		id:      a.notificationID,
1102  		status:  string(s),
1103  		header:  h,
1104  		message: msg,
1105  	}
1106  
1107  	ntfs := a.notifications
1108  	ctx.Async(func() {
1109  		for n := range ntfs {
1110  			time.Sleep(5 * time.Second)
1111  			delete(ntfs, n)
1112  			ctx.Async(func() {
1113  				ctx.Dispatch(func(ctx app.Context) {
1114  					a.notifications = ntfs
1115  				})
1116  			})
1117  		}
1118  	})
1119  }
1120  
1121  // https://play.golang.org/p/Qg_uv_inCek
1122  // contains checks if a string is present in a slice
1123  func sliceContains(s []string, str string) bool {
1124  	for _, v := range s {
1125  		if v == str {
1126  			return true
1127  		}
1128  	}
1129  
1130  	return false
1131  }
1132  
1133  // The main function is the entry point where the app is configured and started.
1134  // It is executed in 2 different environments: A client (the web browser) and a
1135  // server.
1136  func main() {
1137  	// The first thing to do is to associate the hello component with a path.
1138  	//
1139  	// This is done by calling the Route() function,  which tells go-app what
1140  	// component to display for a given path, on both client and server-side.
1141  	app.Route("/", func() app.Composer{
1142  		return &acid{}
1143  	})
1144  
1145  	// Once the routes set up, the next thing to do is to either launch the app
1146  	// or the server that serves the app.
1147  	//
1148  	// When executed on the client-side, the RunWhenOnBrowser() function
1149  	// launches the app,  starting a loop that listens for app events and
1150  	// executes client instructions. Since it is a blocking call, the code below
1151  	// it will never be executed.
1152  	//
1153  	// When executed on the server-side, RunWhenOnBrowser() does nothing, which
1154  	// lets room for server implementation without the need for precompiling
1155  	// instructions.
1156  	app.RunWhenOnBrowser()
1157  
1158  	// Finally, launching the server that serves the app is done by using the Go
1159  	// standard HTTP package.
1160  	//
1161  	// The Handler is an HTTP handler that serves the client and all its
1162  	// required resources to make it work into a web browser. Here it is
1163  	// configured to handle requests with a path that starts with "/".
1164  
1165  	withGz := gziphandler.GzipHandler(&app.Handler{
1166  		Name:        "cyber-acid",
1167  		Description: "Cyber Acid - Liquid democracy politics simulator based on personal reputation index",
1168  		Styles: []string{
1169  			"https://assets.ubuntu.com/v1/vanilla-framework-version-3.8.0.min.css",
1170  			"https://use.fontawesome.com/releases/v6.2.0/css/all.css",
1171  		},
1172  		Scripts: []string{},
1173  	})
1174  	http.Handle("/", withGz)
1175  
1176  	if err := http.ListenAndServe(":7000", nil); err != nil {
1177  		log.Fatal(err)
1178  	}
1179  }