🎉🚀 May the concensus be with us!

This commit is contained in:
Brian Wiborg 2024-05-13 00:47:24 +02:00
commit 48328e7db2
No known key found for this signature in database
GPG Key ID: BE53FA9286B719D6
33 changed files with 4051 additions and 0 deletions

169
.gitignore vendored Normal file
View File

@ -0,0 +1,169 @@
# Created by https://www.toptal.com/developers/gitignore/api/go,node
# Edit at https://www.toptal.com/developers/gitignore?templates=go,node
### Go ###
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# Go workspace file
go.work
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
### Node Patch ###
# Serverless Webpack directories
.webpack/
# Optional stylelint cache
# SvelteKit build / generate output
.svelte-kit
# End of https://www.toptal.com/developers/gitignore/api/go,node
*.db

16
cmd/main.go Normal file
View File

@ -0,0 +1,16 @@
package cmd
import (
"github.com/urfave/cli/v2"
)
var App = cli.App{
Name: "govote",
Usage: "🌈 Referendums and concensus.",
Commands: []*cli.Command{
newCmd,
showCmd,
voteCmd,
serveCmd,
},
}

108
cmd/new.go Normal file
View File

@ -0,0 +1,108 @@
package cmd
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"time"
"code.c-base.org/baccenfutter/govote/store"
"code.c-base.org/baccenfutter/govote/utils"
"code.c-base.org/baccenfutter/govote/voting/quorum"
"code.c-base.org/baccenfutter/govote/voting/threshold"
"github.com/urfave/cli/v2"
)
var newCmd = &cli.Command{
Name: "new",
Usage: " Create a voting",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "deadline",
Usage: "Duration for which this voting is open",
Aliases: []string{"D"},
Value: "1m",
},
&cli.StringFlag{
Name: "quorum",
Usage: "Minimum required number of participants",
Aliases: []string{"Q"},
Value: "SIMPLE",
},
&cli.StringFlag{
Name: "threshold",
Usage: "Minimum number of positive votes",
Aliases: []string{"T"},
Value: "SIMPLE",
},
&cli.StringFlag{
Name: "electors",
Usage: "Comma-separated list of eligible electors or empty if anyone can vote",
Aliases: []string{"E"},
},
&cli.BoolFlag{
Name: "anonymous",
Usage: "Public visibility of votes.",
Aliases: []string{"A"},
Value: false,
},
},
Action: func(ctx *cli.Context) error {
deadline := ctx.String("deadline")
deadlineNum := fmt.Sprintf("%s", deadline[:len(deadline)-1])
deadlineUnit := fmt.Sprintf("%s", deadline[len(deadline)-1:])
deadlineInt, err := strconv.Atoi(deadlineNum)
if err != nil {
return err
}
if !strings.Contains("mhd", deadlineUnit) {
return fmt.Errorf("invalid deadline unit '%s'. use one of: [ m | d | h ]", deadlineUnit)
}
var d time.Time
switch deadlineUnit {
case "m", "":
d = time.Now().UTC().Add(time.Duration(deadlineInt)*time.Minute).Round(time.Second)
case "h":
d = time.Now().UTC().Add(time.Duration(deadlineInt)*time.Hour).Round(time.Second)
case "d":
d = time.Now().UTC().Add(time.Duration(deadlineInt)*time.Hour*24).Round(time.Second)
default:
panic("this code should never be reached")
}
var (
q quorum.Quorum
t threshold.Threshold
)
if q, err = quorum.FromString(ctx.String("quorum")); err != nil {
return err
}
if t, err = threshold.FromString(ctx.String("threshold")); err != nil {
return err
}
e := strings.Split(ctx.String("electors"), " ")
a := ctx.Bool("anonymous")
var r = ""
if ctx.Args().Len() == 0 {
fmt.Print("Give your referendum a concise name or subject: ")
inputReader := bufio.NewReader(os.Stdin)
r, _ = inputReader.ReadString('\n')
r = r[:len(r)-1]
} else {
r = strings.Join(ctx.Args().Slice(), " ")
}
id := utils.GenerateRandomString(11)
if err := store.NewVoting(string(id), r, d, q, t, e, a); err != nil {
return err
}
fmt.Println(string(id))
return nil
},
}

21
cmd/serve.go Normal file
View File

@ -0,0 +1,21 @@
package cmd
import (
"code.c-base.org/baccenfutter/govote/http"
"github.com/urfave/cli/v2"
)
var serveCmd = &cli.Command{
Name: "serve",
Usage: "Start the HTTP server",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "bind-address",
Usage: "The TCP address:port to bind to",
Value: ":3000",
},
},
Action: func(ctx *cli.Context) error {
return http.Serve(ctx.String("bind-address"))
},
}

34
cmd/show.go Normal file
View File

@ -0,0 +1,34 @@
package cmd
import (
"bufio"
"fmt"
"os"
"code.c-base.org/baccenfutter/govote/store"
"github.com/urfave/cli/v2"
)
var showCmd = &cli.Command{
Name: "show",
Usage: "📈 Display a voting",
Action: func(ctx *cli.Context) error {
var r string
if ctx.Args().Len() == 0 {
inputReader := bufio.NewReader(os.Stdin)
r, _ = inputReader.ReadString('\n')
r = r[:len(r)-1]
}
id := ctx.Args().Get(0)
if id == "" {
return fmt.Errorf("Please provide an ID!")
}
voting, err := store.GetVoting(id)
if err != nil {
return err
}
fmt.Println(voting)
return nil
},
}

46
cmd/vote.go Normal file
View File

@ -0,0 +1,46 @@
package cmd
import (
"code.c-base.org/baccenfutter/govote/store"
"code.c-base.org/baccenfutter/govote/voting/vote"
"github.com/google/uuid"
"github.com/urfave/cli/v2"
)
var voteCmd = &cli.Command{
Name: "vote",
Usage: "📄 Place vote",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "voting-id",
Usage: "Voting ID",
Aliases: []string{"V"},
Required: true,
},
&cli.StringFlag{
Name: "elector",
Usage: "Elector",
Aliases: []string{"E"},
Required: true,
},
&cli.StringFlag{
Name: "choice",
Usage: "Choice",
Aliases: []string{"C"},
Required: true,
},
},
Action: func(ctx *cli.Context) error {
var (
id = uuid.New().String()
votingID = ctx.String("voting-id")
elector = ctx.String("elector")
)
choice, err := vote.ChoiceFromString(ctx.String("choice"))
if err != nil {
return err
}
err = store.PlaceVote(id, votingID, elector, choice)
return err
},
}

15
global.css Normal file
View File

@ -0,0 +1,15 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
@apply h-full;
}
body {
@apply h-full;
}
div#__next {
@apply h-full;
}
main {
@apply h-full;
}

29
go.mod Normal file
View File

@ -0,0 +1,29 @@
module code.c-base.org/baccenfutter/govote
go 1.22.2
require (
github.com/google/uuid v1.6.0
github.com/labstack/echo v3.3.10+incompatible
github.com/mattn/go-sqlite3 v1.14.22
github.com/urfave/cli/v2 v2.27.2
robpike.io/filter v0.0.0-20210831053821-dcb4225e6ac8
)
require (
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
github.com/labstack/echo/v4 v4.12.0 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
)

51
go.sum Normal file
View File

@ -0,0 +1,51 @@
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/labstack/echo v3.3.10+incompatible h1:pGRcYk231ExFAyoAjAfD85kQzRJCRI8bbnE7CX5OEgg=
github.com/labstack/echo v3.3.10+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
robpike.io/filter v0.0.0-20210831053821-dcb4225e6ac8 h1:lwm39gwvSdkVwepWIJM43BTc/1J7f6OCzMNUCox7ozI=
robpike.io/filter v0.0.0-20210831053821-dcb4225e6ac8/go.mod h1:1GvacT5fu9sizB4SqyrJVJk/CQN2ZN4Z/zld7fYxzLo=

137
http/main.go Normal file
View File

@ -0,0 +1,137 @@
package http
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"code.c-base.org/baccenfutter/govote/store"
"code.c-base.org/baccenfutter/govote/utils"
"code.c-base.org/baccenfutter/govote/voting"
"code.c-base.org/baccenfutter/govote/voting/quorum"
"code.c-base.org/baccenfutter/govote/voting/threshold"
"code.c-base.org/baccenfutter/govote/voting/vote"
"github.com/google/uuid"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
)
func Serve(bindAddr string) error {
e := echo.New()
e.Pre(middleware.RemoveTrailingSlash())
// e.Use(middleware.Recover())
NewTemplateRenderer(e, "tmpl/*.html")
e.Static("/static", "static")
e.GET("/", handleIndex)
e.GET("/v", handleVotingForm)
e.POST("/v", handleNewVoting)
e.GET("/v/:id", handleShowVoting)
e.POST("/v/:id", handleVote)
return e.Start(bindAddr)
}
func handleIndex(ctx echo.Context) error {
// return ctx.Redirect(http.StatusTemporaryRedirect, "/v")
return ctx.String(200, ctx.Request().Header.Get("X-Remote-User"))
}
func handleNewVoting(ctx echo.Context) error {
id :=utils.GenerateRandomString(11)
var (
formReferendum = ctx.FormValue("referendum")
formDeadline = ctx.FormValue("deadline")
formQuorum = ctx.FormValue("quorum")
formThreshold = ctx.FormValue("threshold")
formElectors = ctx.FormValue("electors")
formAnonymous = ctx.FormValue("anonymous")
)
var (
err error
r string
d time.Time
q quorum.Quorum
t threshold.Threshold
e = []string{}
a bool
)
r = formReferendum
deadlineNum := fmt.Sprintf("%s", formDeadline[:len(formDeadline)-1])
deadlineUnit := fmt.Sprintf("%s", formDeadline[len(formDeadline)-1:])
deadlineInt, err := strconv.Atoi(deadlineNum)
if err != nil {
return err
}
switch deadlineUnit {
case "m", "":
d = time.Now().UTC().Add(time.Duration(deadlineInt)*time.Minute).Round(time.Second)
case "h":
d = time.Now().UTC().Add(time.Duration(deadlineInt)*time.Hour).Round(time.Second)
case "d":
d = time.Now().UTC().Add(time.Duration(deadlineInt)*time.Hour*24).Round(time.Second)
default:
panic("this code should never be reached")
}
if q, err = quorum.FromString(formQuorum); err != nil {
return err
}
if t, err = threshold.FromString(formThreshold); err != nil {
return err
}
e = strings.Split(formElectors, " ")
if formAnonymous == "on" {
a = true
}
store.NewVoting(id, r, d, q, t, e, a)
return ctx.Redirect(http.StatusFound, fmt.Sprintf("/v/%s", id))
}
func handleVotingForm(ctx echo.Context) error {
return ctx.Render(http.StatusOK, "voting_form", nil)
}
func handleVote(ctx echo.Context) error {
var (
id = uuid.New().String()
vid = ctx.Param("id")
elector = ctx.Request().Header.Get("X-Remote-User")
choice = ctx.FormValue("vote")
v *voting.Voting
c vote.Choice
err error
)
v, err = store.GetVoting(vid)
if err != nil {
return err
}
if time.Now().UTC().After(v.Deadline()) {
return ctx.Redirect(http.StatusFound, fmt.Sprintf("/v/%s", vid))
}
if c, err = vote.ChoiceFromString(choice); err != nil {
return err
}
store.PlaceVote(id, vid, elector, c)
return ctx.Render(http.StatusFound, "thanks", map[string]interface{}{
"Id": id,
"Vid": vid,
})
}
func handleShowVoting(ctx echo.Context) error {
v, err := store.GetVoting(ctx.Param("id"))
if err != nil {
fmt.Println(err)
return err
}
return ctx.Render(http.StatusOK, "voting", map[string]interface{}{
"Voting": v,
})
}

31
http/template_renderer.go Normal file
View File

@ -0,0 +1,31 @@
package http
import (
"io"
"text/template"
"github.com/labstack/echo"
)
type Template struct {
Templates *template.Template
}
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.Templates.ExecuteTemplate(w, name, data)
}
func NewTemplateRenderer(e *echo.Echo, paths ...string) {
tmpl := &template.Template{}
for i := range paths {
template.Must(tmpl.ParseGlob(paths[i]))
}
t := newTemplate(tmpl)
e.Renderer = t
}
func newTemplate(templates *template.Template) echo.Renderer {
return &Template{
Templates: templates,
}
}

19
main.go Normal file
View File

@ -0,0 +1,19 @@
package main
import (
"log"
"os"
"code.c-base.org/baccenfutter/govote/cmd"
)
//go:generate yarn run build
//go:generate cp node_modules/htmx.org/dist/htmx.min.js static/
//go:generate cp global.css static/css/
func main() {
app := cmd.App
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}

14
package.json Normal file
View File

@ -0,0 +1,14 @@
{
"name": "govote",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "tailwindcss build -o static/css/tailwind.css",
"watch": "tailwindcss build -o static/css/tailwind.css --watch"
},
"dependencies": {
"flowbite": "^2.3.0",
"tailwindcss": "^3.4.3"
}
}

15
static/css/global.css Normal file
View File

@ -0,0 +1,15 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
@apply h-full;
}
body {
@apply h-full;
}
div#__next {
@apply h-full;
}
main {
@apply h-full;
}

1561
static/css/tailwind.css Normal file

File diff suppressed because it is too large Load Diff

1
static/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

21
store/db.go Normal file
View File

@ -0,0 +1,21 @@
package store
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
)
const file = "./govote.db"
var db *sql.DB
func init() {
var err error
if db, err = sql.Open("sqlite3", file); err != nil {
panic(err)
}
initCreateTables(db)
initStmts(db)
}

110
store/handler.go Normal file
View File

@ -0,0 +1,110 @@
package store
import (
"fmt"
"strings"
"time"
"code.c-base.org/baccenfutter/govote/voting"
"code.c-base.org/baccenfutter/govote/voting/quorum"
"code.c-base.org/baccenfutter/govote/voting/threshold"
"code.c-base.org/baccenfutter/govote/voting/vote"
)
func NewVoting(
id string,
r string,
d time.Time,
q quorum.Quorum,
t threshold.Threshold,
e []string,
a bool,
) error {
electors := strings.Join(e, ";")
if _, err := votingInsert.Exec(id, r, d.String(), q.String(), t.String(), electors, a); err != nil {
return err
}
return nil
}
func GetVoting(id string) (*voting.Voting, error) {
result := votingSelect.QueryRow(id)
if result == nil {
return nil, fmt.Errorf("not found: %s", id)
}
var (
err error
r string
d time.Time
q quorum.Quorum
t threshold.Threshold
e []string
a bool
dbDeadline string
dbQuorum string
dbThreshold string
dbElectors string
)
if err := result.Scan(&r, &dbDeadline, &dbQuorum, &dbThreshold, &dbElectors, &a); err != nil {
return nil, err
}
if d, err = time.Parse("2006-01-02 15:04:05 -0700 MST", dbDeadline); err != nil {
return nil, err
}
if q, err = quorum.FromString(dbQuorum); err != nil {
return nil, err
}
if t, err = threshold.FromString(dbThreshold); err != nil {
return nil, err
}
e = strings.Split(dbElectors, " ")
votes, err := getVotes(id)
if err != nil {
return nil, err
}
v := voting.NewVotingWithVotes(id, r, d, q, t, e, a, votes)
return v, nil
}
func PlaceVote(id, votingID, elector string, choice vote.Choice) error {
if _, err := voteInsert.Exec(id, votingID, elector, choice.String()); err != nil {
return err
}
return nil
}
func getVotes(id string) ([]vote.Vote, error) {
result, err := voteSelect.Query(id)
if err != nil {
return nil, err
}
var (
e string
c vote.Choice
ts time.Time
dbChoice string
dbTimestamp string
votes = []vote.Vote{}
)
for result.Next() {
if err = result.Scan(&e, &dbChoice, &dbTimestamp); err != nil {
return nil, err
}
if c, err = vote.ChoiceFromString(dbChoice); err != nil {
return nil, err
}
if ts, err = time.Parse("2006-01-02 15:04:05", dbTimestamp); err != nil {
return nil, err
}
v := vote.NewVoteWithTimestamp(e, c, ts)
votes = append(votes, v)
}
return votes, nil
}

136
store/prepared.go Normal file
View File

@ -0,0 +1,136 @@
package store
import "database/sql"
var (
votingInsert *sql.Stmt
votingSelect *sql.Stmt
voteEligible *sql.Stmt
voteInsert *sql.Stmt
voteSelect *sql.Stmt
)
func initCreateTables(db *sql.DB) {
var err error
createTables := `
CREATE TABLE IF NOT EXISTS voting (
id TEXT PRIMARY KEY,
referendum TEXT NOT NULL,
created DATETIME WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
deadline DATETIME WITHOUT TIME ZONE NOT NULL,
quorum TEXT NOT NULL DEFAULT 'SIMPLE',
threshold TEXT NOT NULL DEFAULT 'SIMPLE',
electors TEXT,
anonymous BOOL NOT NULL DEFAULT false
);
CREATE TABLE IF NOT EXISTS vote (
id BLOB PRIMARY KEY,
voting TEXT NOT NULL,
elector TEXT NOT NULL,
choice TEXT NOT NULL,
timestamp DATETIME WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(voting) REFERENCES voting(id)
);
CREATE INDEX IF NOT EXISTS vote_voting ON vote ( voting );
`
if _, err = db.Exec(createTables); err != nil {
panic(err)
}
}
func initStmts(db *sql.DB) {
initStmtVotingInsert(db)
initStmtVotingSelect(db)
initStmtVoteEligible(db)
initStmtVoteInsert(db)
initStmtVoteSelect(db)
}
func initStmtVotingInsert(db *sql.DB) {
var err error
if votingInsert, err = db.Prepare(`
INSERT INTO voting (
id,
referendum,
deadline,
quorum,
threshold,
electors,
anonymous
) VALUES (?, ?, ?, ?, ?, ?, ?);
`); err != nil {
panic(err)
}
}
func initStmtVotingSelect(db *sql.DB) {
var err error
if votingSelect, err = db.Prepare(`
SELECT
referendum,
deadline,
quorum,
threshold,
electors,
anonymous
FROM voting
WHERE id = ?;
`); err != nil {
panic(err)
}
}
func initStmtVoteEligible(db *sql.DB) {
var err error
if voteEligible, err = db.Prepare(`
SELECT
id,
referendum,
created,
deadline,
quorum,
threshold
FROM voting
WHERE
id = ?
AND deadline > datetime('now')
AND (
electors IS NULL
OR ',' || electors || ',' LIKE '%,' || ? || ',%'
)
LIMIT 1;
`); err != nil {
panic(err)
}
}
func initStmtVoteInsert(db *sql.DB) {
var err error
if voteInsert, err = db.Prepare(`
INSERT INTO vote (
id, voting, elector, choice
) VALUES (
?, ?, ?, ?
);
`); err != nil {
panic(err)
}
}
func initStmtVoteSelect(db *sql.DB) {
var err error
if voteSelect, err = db.Prepare(`
SELECT
elector,
choice,
timestamp
FROM vote
WHERE voting = ?;
`); err != nil {
panic(err)
}
}

13
tailwind.config.js Normal file
View File

@ -0,0 +1,13 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
'./tmpl/*.html'
],
theme: {
extend: {},
},
plugins: [
require('flowbite/plugin')
],
}

14
tmpl/head.html Normal file
View File

@ -0,0 +1,14 @@
{{ define "head" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="/static/css/tailwind.css">
<script src="/static/htmx.min.js"></script>
<title>{{ or .Title "govote"}}</title>
</head>
<body>
<main>
{{ end }}

5
tmpl/tail.html Normal file
View File

@ -0,0 +1,5 @@
{{ define "tail" }}
</main>
</body>
</html>
{{ end }}

27
tmpl/thanks.html Normal file
View File

@ -0,0 +1,27 @@
{{ define "thanks" }}
{{ template "head" . }}
<div class="flex flex-col justify-center align-center items-center h-screen bg-slate-900 p-2">
<div class="w-full md:w-3/4 lg:w-1/2 xl:w-1/3 border rounded-lg p-2 sm:p-6 md:p-8 bg-slate-800 border-purple-900 text-white">
<div class="relative sm:rounded-lg">
<h1 class="text-center text-4xl text-white pb-2">
Thank you!
</h1>
<p class="p-4 text-gray-400 text-center">
You have made a small voting script very happy! *nom*
</p>
<p class="text-gray-400 text-center">
Your vote has been placed under the ID:<br>
</p>
<p class="text-white text-center text-2xl bold">
{{ .Id }}
</p>
<hr class="m-4">
<form action="/v/{{ .Vid }}" method="GET">
<button type="submit" class="w-full hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center bg-purple-600 hover:bg-purple-700 focus:ring-blue-800 text-white hover:text-yellow-200 text-xl">Go back</button>
</form>
</div>
</div>
</div>
{{ template "tail" . }}
{{ end }}

121
tmpl/voting.html Normal file
View File

@ -0,0 +1,121 @@
{{ define "voting" }}
{{ template "head" . }}
<div class="flex flex-col justify-center align-center items-center h-screen bg-slate-900 p-2">
<div class="w-full md:w-3/4 lg:w-1/2 xl:w-1/3 border rounded-lg p-2 sm:p-6 md:p-8 bg-slate-800 border-purple-900 text-white">
<div class="relative sm:rounded-lg">
<h1 class="text-center text-4xl text-white pb-2">
{{ .Voting.Referendum }}
</h1>
<p class="text-gray-400 text-center text-xs">
{{ .Voting.Deadline }}
</p>
{{ if .Voting.IsOpen }}
{{ template "vote_form" . }}
{{ else }}
{{ template "voting_result" . }}
{{ end }}
</div>
</div>
</div>
{{ template "tail" . }}
{{ end }}
{{ define "vote_form" }}
<div class="relative overflow-x-auto py-4">
<table class="w-full text-sm text-left rtl:text-right text-gray-400 text-center">
<thead class="text-xs uppercase text-gray-400">
<tr>
<th scope="col" class="px-6 py-3">
Quorum
</th>
<th scope="col" class="px-6 py-3">
Threshold
</th>
<th scope="col" class="px-6 py-3">
Anonymous
</th>
</tr>
</thead>
<tbody>
<tr class="border-b bg-gray-800 border-gray-700">
<td scope="row" class="px-6 py-2 font-medium whitespace-nowrap text-white">
SIMPLE
</td>
<td scope="row" class="px-6 py-2 font-medium whitespace-nowrap text-white">
SIMPLE
</td>
<td scope="row" class="px-6 py-2 font-medium whitespace-nowrap text-white">
YES
</td>
</tr>
</tbody>
</table>
</div>
<form action="/v/{{ .Voting.ID }}" method="POST">
<ul class="grid w-full gap-6 md:grid-cols-3 pb-8">
<li>
<input type="radio" id="vote-yes" name="vote" value="YIP" class="hidden peer" required />
<label for="vote-yes" class="inline-flex items-center justify-between w-full p-5 border rounded-lg cursor-pointer border-green-700 hover:border-gray-400 peer-checked:text-white peer-checked:bg-green-700 hover:text-white text-green-700">
<div class="block mx-auto">
<div class="w-full text-lg font-semibold">YIP</div>
</div>
</label>
</li>
<li>
<input type="radio" id="vote-no" name="vote" value="NOPE" class="hidden peer" required />
<label for="vote-no" class="inline-flex items-center justify-between w-full p-5 border rounded-lg cursor-pointer border-red-800 hover:border-gray-400 peer-checked:text-white peer-checked:bg-red-800 hover:text-white text-red-800">
<div class="block mx-auto">
<div class="w-full text-lg font-semibold">NOPE</div>
</div>
</label>
</li>
<li>
<input type="radio" id="vote-abstain" name="vote" value="DUNNO" class="hidden peer" required />
<label for="vote-abstain" class="inline-flex items-center justify-between w-full p-5 border rounded-lg cursor-pointer border-yellow-600 hover:border-gray-400 peer-checked:text-white peer-checked:bg-yellow-600 hover:text-white text-yellow-600">
<div class="block mx-auto">
<div class="w-full text-lg font-semibold">DUNNO</div>
</div>
</label>
</li>
</ul>
<button type="submit" class="w-full hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center bg-purple-600 hover:bg-purple-700 focus:ring-blue-800 text-white hover:text-yellow-200 text-xl">Vote</button>
</form>
{{ end }}
{{ define "voting_result" }}
<table class="w-full text-sm text-left rtl:text-right text-gray-400 text-center mt-4">
<tbody>
<tr class="border-b border-t bg-gray-800 border-gray-700">
<td class="px-6 py-4 text-4xl bold text-green-700">
{{ .Voting.Result.Yes }}
</td>
<td class="px-6 py-4 text-4xl bold text-red-800">
{{ .Voting.Result.No }}
</td>
<td class="px-6 py-4 text-4xl bold text-yellow-700">
{{ .Voting.Result.Abstain }}
</td>
</tr>
</tbody>
</table>
{{ if and .Voting.Result.Quorum .Voting.Result.Threshold }}
<p class="text-4xl bold text-center p-4 text-green-700 tracking-widest">
ACCEPTED
</p>
{{ else }}
<p class="text-4xl bold text-center p-4 text-red-800 tracking-widest">
REJECTED
</p>
{{ end }}
{{ if .Voting.Result.Quorum }}
<p class="text-center text-green-700">
Quorum: REACHED
</p>
{{ else }}
<p class="text-center text-red-800">
Quorum: FAILED
</p>
{{ end }}
{{ end }}

84
tmpl/voting_form.html Normal file
View File

@ -0,0 +1,84 @@
{{ define "voting_form" }}
{{ template "head" . }}
<div class="flex flex-col justify-center align-center items-center h-screen bg-slate-900 p-2">
<div class="w-full md:w-3/4 lg:w-1/2 xl:w-1/3 border rounded-lg p-2 sm:p-6 md:p-8 bg-slate-800 border-purple-900">
<form class="space-y-6" action="/v" method="POST">
<h5 class="text-xl font-medium text-white">New Voting</h5>
<div>
<label for="referendum" class="block mb-2 text-sm font-medium text-white">Referendum</label>
<input type="text" name="referendum" id="referendum" class="border text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 bg-gray-600 border-gray-500 placeholder-gray-400 text-white" placeholder="Concise question or subject..." required autofocus />
</div>
<div class="flex flex-row justify-between">
<div class="w-full px-2">
<label for="deadline" class="block mb-2 text-sm font-medium text-white">Deadline</label>
<input type="text" name="deadline" id="deadline" class="border text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 bg-gray-600 border-gray-500 placeholder-gray-400 text-white text-center" placeholder="Deadline, e.g.: 3m, 1h, 7d, etc" value="1m" required />
</div>
<div class="w-full px-2">
<label for="quorum" class="block mb-2 text-sm font-medium text-white">Quorum</label>
<select id="quorum" name="quorum" class="border text-sm rounded-lg block w-full p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500">
<option selected>SIMPLE</option>
<option value="1/5">1/5</option>
<option value="1/4">1/4</option>
<option value="1/3">1/3</option>
<option value="1/2">1/2</option>
<option value="2/5">2/5</option>
<option value="2/3">2/3</option>
<option value="3/5">3/5</option>
<option value="3/4">3/4</option>
<option value="4/5">4/5</option>
</select>
</div>
<div class="w-full px-2">
<label for="threshold" class="block mb-2 text-sm font-medium text-white">Threshold</label>
<select id="threshold" name="threshold" class="border text-sm rounded-lg block w-full p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500">
<option selected>SIMPLE</option>
<option value="1/5">1/5</option>
<option value="1/4">1/4</option>
<option value="1/3">1/3</option>
<option value="1/2">1/2</option>
<option value="2/5">2/5</option>
<option value="2/3">2/3</option>
<option value="3/5">3/5</option>
<option value="3/4">3/4</option>
<option value="4/5">4/5</option>
</select>
</div>
</div>
<div>
<label for="electors" class="block mb-2 text-sm font-medium text-white">Who can vote? <small>(empty if anyone can vote)</small></label>
<div class="flex">
<span class="inline-flex items-center px-3 text-sm border rounded-e-0 border-e-0 rounded-s-md bg-gray-600 text-gray-400 border-gray-600">
<svg class="w-4 h-4 text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 0a10 10 0 1 0 10 10A10.011 10.011 0 0 0 10 0Zm0 5a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm0 13a8.949 8.949 0 0 1-4.951-1.488A3.987 3.987 0 0 1 9 13h2a3.987 3.987 0 0 1 3.951 3.512A8.949 8.949 0 0 1 10 18Z"/>
</svg>
</span>
<input type="text" id="electores" name="electors" class="rounded-none rounded-e-lg border block flex-1 min-w-0 w-full text-sm border-gray-300 p-2.5 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500" placeholder="Names separated by spaces...">
</div>
</div>
<div>
<label class="inline-flex items-center cursor-pointer">
<input type="checkbox" value="anonymous" class="sr-only peer">
<div class="relative w-11 h-6 peer-focus:ring-purple-800 rounded-full peer bg-gray-700 peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-purple-600"></div>
<span class="ms-3 text-sm font-medium text-gray-400">Anonymous vote</span>
</label>
</div>
<button type="submit" class="w-full hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center bg-purple-600 hover:bg-purple-700 focus:ring-blue-800 text-white hover:text-yellow-200 text-xl">Create</button>
<p class="text-gray-400 italic">
Quotum:
The minumum number of eligible votes required for the referendum to pass.
(SIMPLE means that at least one vote is required.)
</p>
<p class="text-gray-400 italic">
Threshold:
The minumum number of YES votes required for the referendum to pass.
(SIMPLE means that there must be more YES than NO votes.)
</p>
<p class="text-gray-400 italic">
Anonymous:
Individual votes by the electors are not made public.
</p>
</form>
</div>
</div>
{{ template "tail" . }}
{{ end }}

15
utils/random.go Normal file
View File

@ -0,0 +1,15 @@
package utils
import (
"math/rand"
)
const charSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
func GenerateRandomString(length int) string {
result := make([]byte, length)
for i := 0; i < length; i++ {
result[i] = charSet[rand.Intn(len(charSet))]
}
return string(result)
}

203
voting/main.go Normal file
View File

@ -0,0 +1,203 @@
package voting
import (
"fmt"
"strings"
"time"
"code.c-base.org/baccenfutter/govote/voting/quorum"
"code.c-base.org/baccenfutter/govote/voting/threshold"
"code.c-base.org/baccenfutter/govote/voting/vote"
"robpike.io/filter"
)
type (
Voting struct {
id string
referendum string
deadline time.Time
quorum quorum.Quorum
threshold threshold.Threshold
electors []string
annonymous bool
votes []vote.Vote
}
)
func NewVoting(id, r string, d time.Time, q quorum.Quorum, t threshold.Threshold, e []string, a bool) *Voting {
return &Voting{
id: id,
referendum: r,
deadline: d,
quorum: q,
threshold: t,
electors: e,
annonymous: a,
votes: []vote.Vote{},
}
}
func NewVotingWithVotes(id, r string, d time.Time, q quorum.Quorum, t threshold.Threshold, e []string, a bool, v []vote.Vote) *Voting {
voting := NewVoting(id, r, d, q, t, e, a)
for i := range v {
voting.votes = append(voting.votes, v[i])
}
return voting
}
func (v Voting) String() string {
var (
possibleVotes int = len(v.electors)
totalVotes int = len(v.Votes())
yesVotes int = len(v.yesVotes())
noVotes int = len(v.noVotes())
deadlineStatus string = "🎭 ONGOING 🎭"
quorumStatus string = "❌ FAIL"
thresholdStatus string = "❌ FAIL"
out string = ""
)
quorumSatisfied := v.quorum.IsSatisfied(possibleVotes, totalVotes)
thresholdSatisfied := v.threshold.IsSatisfied(totalVotes, yesVotes, noVotes)
votingSatisfied := quorumSatisfied && thresholdSatisfied
if time.Now().UTC().After(v.deadline) {
if votingSatisfied {
deadlineStatus = "✅ APROVED ✅"
} else {
deadlineStatus = "❌ REJECTED ❌"
}
}
if v.quorum.IsSatisfied(possibleVotes, totalVotes) {
quorumStatus = "✅ PASS"
}
if v.threshold.IsSatisfied(totalVotes, yesVotes, noVotes) {
thresholdStatus = "✅ PASS"
}
out += fmt.Sprintf("Referendum: %s\n", strings.ToUpper(v.referendum))
out += fmt.Sprintf("Deadline : %s UTC\n", v.deadline.Format(time.DateTime))
out += fmt.Sprintf("Quorum : %s (%d/%d) (required: %s)\n", quorumStatus, totalVotes, possibleVotes, v.quorum)
out += fmt.Sprintf("Threshold : %s (%d/%d) (required: %s)\n", thresholdStatus, yesVotes, totalVotes, v.threshold)
out += fmt.Sprintf("Electors : [ %d ] %s\n", len(v.electors), v.electors)
out += fmt.Sprintf(
"Votes : [ %d | %d | %d ] (❎|❌|❔)\n",
len(v.yesVotes()),
len(v.noVotes()),
len(v.abstainVotes()),
)
out += fmt.Sprintf("Status : %s\n", deadlineStatus)
if !v.annonymous {
out += "\n"
if v.votes != nil && len(v.votes) > 0 {
for _, _v := range v.votes {
out += fmt.Sprintf("💬 %s\n", _v)
}
}
}
return out
}
func (v Voting) ID() string {
return v.id
}
func (v Voting) Referendum() string {
return v.referendum
}
func (v Voting) Deadline() time.Time {
return v.deadline
}
func (v Voting) Quorum() string {
return v.quorum.String()
}
func (v Voting) Threshold() string {
return v.threshold.String()
}
func (v Voting) Electors() []string {
electors := make([]string, len(v.electors))
for i := range v.electors {
electors[i] = v.electors[i]
}
return electors
}
func (v Voting) IsOpen() bool {
return v.deadline.After(time.Now().UTC())
}
func (v Voting) Result() Result {
var (
possibleVotes = len(v.electors)
totalVotes = len(v.Votes())
yesVotes = len(v.yesVotes())
noVotes = len(v.noVotes())
votes []vote.Vote
quorumSatisfied = v.quorum.IsSatisfied(possibleVotes, totalVotes)
thresholdSatisfied = v.threshold.IsSatisfied(totalVotes, yesVotes, noVotes)
)
if !v.annonymous {
votes = v.Votes()
}
return Result{
Quorum: quorumSatisfied,
Threshold: thresholdSatisfied,
Yes: len(v.yesVotes()),
No: len(v.noVotes()),
Abstain: len(v.Votes()) - len(v.yesVotes()) - len(v.noVotes()),
Votes: votes,
}
}
func (v *Voting) Vote(vote vote.Vote) error {
if time.Now().UTC().After(v.deadline) {
return fmt.Errorf("deadline has passed")
}
for _, elector := range v.electors {
if elector == vote.Elector {
v.votes = append(v.votes, vote)
return nil
}
}
return fmt.Errorf("not eligable to vote: %s", vote.Elector)
}
func (v Voting) Votes() []vote.Vote {
votes := []vote.Vote{}
nextVote:
for i := len(v.votes)-1; i >= 0; i-- {
elector := v.votes[i].Elector
for _, e := range votes {
if e.Elector == elector {
continue nextVote
}
}
votes = append(votes, v.votes[i])
}
return votes
}
func (v Voting) yesVotes() []vote.Vote {
filterFunc := func(_v vote.Vote) bool { return _v.Choice == vote.Yes }
return filter.Choose(v.Votes(), filterFunc).([]vote.Vote)
}
func (v Voting) noVotes() []vote.Vote {
filterFunc := func(_v vote.Vote) bool { return _v.Choice == vote.No }
return filter.Choose(v.Votes(), filterFunc).([]vote.Vote)
}
func (v Voting) abstainVotes() []vote.Vote {
filterFunc := func(_v vote.Vote) bool { return _v.Choice == vote.Abstain }
return filter.Choose(v.Votes(), filterFunc).([]vote.Vote)
}

103
voting/quorum/quorum.go Normal file
View File

@ -0,0 +1,103 @@
package quorum
import (
"fmt"
"strings"
)
type Quorum uint8
const (
Simple Quorum = iota
OneFifth
OneQuarter
OneThird
OneHalf
TwoFifths
TwoThirds
ThreeQuarters
ThreeFifths
FourFifths
Unanimous
)
func ValidQuorums() []Quorum {
return []Quorum{
Simple,
OneFifth, OneQuarter, OneThird, OneHalf,
TwoThirds, TwoFifths,
ThreeQuarters, ThreeFifths,
FourFifths,
}
}
func FromString(s string) (Quorum, error) {
for _, q := range ValidQuorums() {
if strings.ToUpper(q.String()) == strings.ToUpper(s) {
return q, nil
}
}
return Simple, fmt.Errorf("inalid quorum: %s", s)
}
func (q Quorum) String() string {
switch q {
case Simple:
return "SIMPLE"
case OneFifth:
return "1/5"
case OneQuarter:
return "1/4"
case OneThird:
return "1/3"
case OneHalf:
return "1/2"
case TwoThirds:
return "2/3"
case TwoFifths:
return "2/5"
case ThreeQuarters:
return "3/4"
case ThreeFifths:
return "3/5"
case FourFifths:
return "4/5"
case Unanimous:
return "ALL"
default:
panic("this code should never be reached")
}
}
func (q Quorum) IsSatisfied(possibleVotes, totalVotes int) bool {
if totalVotes == 0 {
return false
}
switch q {
case Simple:
return true
case OneFifth:
return totalVotes * 5 >= possibleVotes
case OneQuarter:
return totalVotes * 4 >= possibleVotes
case OneThird:
return totalVotes * 3 >= possibleVotes
case OneHalf:
return totalVotes * 2 >= possibleVotes
case TwoThirds:
return totalVotes * 3 >= possibleVotes * 2
case TwoFifths:
return totalVotes * 5 >= possibleVotes * 2
case ThreeQuarters:
return totalVotes * 4 >= possibleVotes * 3
case ThreeFifths:
return totalVotes * 5 >= possibleVotes * 3
case FourFifths:
return totalVotes * 5 >= possibleVotes * 4
case Unanimous:
return totalVotes >= possibleVotes
default:
panic("this code should never be reached⚜")
}
return false
}

10
voting/result.go Normal file
View File

@ -0,0 +1,10 @@
package voting
import "code.c-base.org/baccenfutter/govote/voting/vote"
type Result struct {
Quorum bool
Threshold bool
Yes, No, Abstain int
Votes []vote.Vote
}

View File

@ -0,0 +1,102 @@
package threshold
import (
"fmt"
"strings"
)
type Threshold uint8
const (
Simple Threshold = iota
OneFifth
OneQuarter
OneThird
OneHalf
TwoThirds
TwoFifths
ThreeQuarters
ThreeFifths
FourFifths
Unanimous
)
func ValidThresholds() []Threshold {
return []Threshold{
Simple,
OneFifth, OneQuarter, OneThird, OneHalf,
TwoFifths, TwoThirds,
ThreeFifths, ThreeQuarters, ThreeFifths,
FourFifths,
}
}
func FromString(s string) (Threshold, error) {
for _, t := range ValidThresholds() {
if strings.ToUpper(t.String()) == strings.ToUpper(s) {
return t, nil
}
}
return Simple, fmt.Errorf("invalid threshold: %s", s)
}
func (t Threshold) String() string {
switch t {
case Simple:
return "SIMPLE"
case OneFifth:
return "1/5"
case OneQuarter:
return "1/4"
case OneThird:
return "1/3"
case OneHalf:
return "1/2"
case TwoThirds:
return "2/3"
case TwoFifths:
return "2/5"
case ThreeQuarters:
return "3/4"
case ThreeFifths:
return "3/5"
case FourFifths:
return "4/5"
case Unanimous:
return "ALL"
default:
panic("this code should never be reached")
}
}
func (t Threshold) IsSatisfied(totalVotes, yesVotes, noVotes int) bool {
if totalVotes == 0 {
return false
}
switch t {
case Simple:
return yesVotes > noVotes
case OneFifth:
return yesVotes * 5 >= totalVotes
case OneQuarter:
return yesVotes * 4 >= totalVotes
case OneThird:
return yesVotes * 3 >= totalVotes
case OneHalf:
return yesVotes * 2 >= totalVotes
case TwoThirds:
return yesVotes * 3 >= totalVotes * 2
case TwoFifths:
return yesVotes * 5 >= totalVotes * 2
case ThreeQuarters:
return yesVotes * 4 >= totalVotes * 3
case ThreeFifths:
return yesVotes * 5 >= totalVotes * 3
case FourFifths:
return yesVotes * 5 >= totalVotes * 4
case Unanimous:
return yesVotes >= totalVotes
default:
panic("this code should never be reached")
}
}

40
voting/vote/choice.go Normal file
View File

@ -0,0 +1,40 @@
package vote
import (
"fmt"
"strings"
)
type Choice int8
func (choice Choice) String() string {
switch choice {
case Yes:
return "YIP"
case No:
return "NOPE"
case Abstain:
return "DUNNO"
default:
panic("this code should never be reached")
}
}
const (
Abstain Choice = 0
Yes Choice = 1
No Choice = -1
)
func ValidChoices() []Choice {
return []Choice{Yes, No, Abstain}
}
func ChoiceFromString(s string) (Choice, error) {
for _, c := range ValidChoices() {
if strings.ToUpper(c.String()) == strings.ToUpper(s) {
return c, nil
}
}
return Abstain, fmt.Errorf("invalid choice: %s", s)
}

32
voting/vote/vote.go Normal file
View File

@ -0,0 +1,32 @@
package vote
import (
"fmt"
"time"
)
type Vote struct {
Elector string
Choice Choice
timestamp time.Time
}
func NewVote(elector string, choice Choice) Vote {
return Vote{
Elector: elector,
Choice: choice,
timestamp: time.Now().UTC(),
}
}
func NewVoteWithTimestamp(elector string, choice Choice, timestamp time.Time) Vote {
return Vote{
Elector: elector,
Choice: choice,
timestamp: timestamp,
}
}
func (vote Vote) String() string {
return fmt.Sprintf("%s %s %s", vote.timestamp.Format(time.DateTime), vote.Choice, vote.Elector)
}

748
yarn.lock Normal file
View File

@ -0,0 +1,748 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@alloc/quick-lru@^5.2.0":
version "5.2.0"
resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30"
integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550"
integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==
dependencies:
string-width "^5.1.2"
string-width-cjs "npm:string-width@^4.2.0"
strip-ansi "^7.0.1"
strip-ansi-cjs "npm:strip-ansi@^6.0.1"
wrap-ansi "^8.1.0"
wrap-ansi-cjs "npm:wrap-ansi@^7.0.0"
"@jridgewell/gen-mapping@^0.3.2":
version "0.3.5"
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36"
integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==
dependencies:
"@jridgewell/set-array" "^1.2.1"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@jridgewell/trace-mapping" "^0.3.24"
"@jridgewell/resolve-uri@^3.1.0":
version "3.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6"
integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
"@jridgewell/set-array@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280"
integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==
"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14":
version "1.4.15"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@jridgewell/trace-mapping@^0.3.24":
version "0.3.25"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0"
integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==
dependencies:
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@nodelib/fs.scandir@2.1.5":
version "2.1.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
dependencies:
"@nodelib/fs.stat" "2.0.5"
run-parallel "^1.1.9"
"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b"
integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
"@nodelib/fs.walk@^1.2.3":
version "1.2.8"
resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a"
integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
dependencies:
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@pkgjs/parseargs@^0.11.0":
version "0.11.0"
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@popperjs/core@^2.9.3":
version "2.11.8"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
ansi-regex@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
ansi-regex@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a"
integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==
ansi-styles@^4.0.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937"
integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
dependencies:
color-convert "^2.0.1"
ansi-styles@^6.1.0:
version "6.2.1"
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
any-promise@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f"
integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==
anymatch@~3.1.2:
version "3.1.3"
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e"
integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
dependencies:
normalize-path "^3.0.0"
picomatch "^2.0.4"
arg@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c"
integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
binary-extensions@^2.0.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522"
integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
brace-expansion@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
dependencies:
balanced-match "^1.0.0"
braces@^3.0.2, braces@~3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==
dependencies:
fill-range "^7.0.1"
camelcase-css@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5"
integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
chokidar@^3.5.3:
version "3.6.0"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b"
integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
glob-parent "~5.1.2"
is-binary-path "~2.1.0"
is-glob "~4.0.1"
normalize-path "~3.0.0"
readdirp "~3.6.0"
optionalDependencies:
fsevents "~2.3.2"
color-convert@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3"
integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
dependencies:
color-name "~1.1.4"
color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
commander@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068"
integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
cross-spawn@^7.0.0:
version "7.0.3"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6"
integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==
dependencies:
path-key "^3.1.0"
shebang-command "^2.0.0"
which "^2.0.1"
cssesc@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
didyoumean@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==
dlv@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79"
integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
eastasianwidth@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
emoji-regex@^8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
emoji-regex@^9.2.2:
version "9.2.2"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
fast-glob@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129"
integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==
dependencies:
"@nodelib/fs.stat" "^2.0.2"
"@nodelib/fs.walk" "^1.2.3"
glob-parent "^5.1.2"
merge2 "^1.3.0"
micromatch "^4.0.4"
fastq@^1.6.0:
version "1.17.1"
resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47"
integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==
dependencies:
reusify "^1.0.4"
fill-range@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40"
integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==
dependencies:
to-regex-range "^5.0.1"
flowbite@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/flowbite/-/flowbite-2.3.0.tgz#0730e35d8b0d1dcdea26bb27d848bd9c0141cde1"
integrity sha512-pm3JRo8OIJHGfFYWgaGpPv8E+UdWy0Z3gEAGufw+G/1dusaU/P1zoBLiQpf2/+bYAi+GBQtPVG86KYlV0W+AFQ==
dependencies:
"@popperjs/core" "^2.9.3"
mini-svg-data-uri "^1.4.3"
foreground-child@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d"
integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==
dependencies:
cross-spawn "^7.0.0"
signal-exit "^4.0.1"
fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c"
integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
glob-parent@^5.1.2, glob-parent@~5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
dependencies:
is-glob "^4.0.1"
glob-parent@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3"
integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
dependencies:
is-glob "^4.0.3"
glob@^10.3.10:
version "10.3.12"
resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.12.tgz#3a65c363c2e9998d220338e88a5f6ac97302960b"
integrity sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==
dependencies:
foreground-child "^3.1.0"
jackspeak "^2.3.6"
minimatch "^9.0.1"
minipass "^7.0.4"
path-scurry "^1.10.2"
hasown@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003"
integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
dependencies:
function-bind "^1.1.2"
is-binary-path@~2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09"
integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
dependencies:
binary-extensions "^2.0.0"
is-core-module@^2.13.0:
version "2.13.1"
resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384"
integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==
dependencies:
hasown "^2.0.0"
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-fullwidth-code-point@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
jackspeak@^2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8"
integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==
dependencies:
"@isaacs/cliui" "^8.0.2"
optionalDependencies:
"@pkgjs/parseargs" "^0.11.0"
jiti@^1.21.0:
version "1.21.0"
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d"
integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==
lilconfig@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"
integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==
lilconfig@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.1.tgz#9d8a246fa753106cfc205fd2d77042faca56e5e3"
integrity sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==
lines-and-columns@^1.1.6:
version "1.2.4"
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
lru-cache@^10.2.0:
version "10.2.2"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878"
integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==
merge2@^1.3.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
micromatch@^4.0.4, micromatch@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6"
integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==
dependencies:
braces "^3.0.2"
picomatch "^2.3.1"
mini-svg-data-uri@^1.4.3:
version "1.4.4"
resolved "https://registry.yarnpkg.com/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz#8ab0aabcdf8c29ad5693ca595af19dd2ead09939"
integrity sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==
minimatch@^9.0.1:
version "9.0.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51"
integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==
dependencies:
brace-expansion "^2.0.1"
"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.4:
version "7.1.0"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.0.tgz#b545f84af94e567386770159302ca113469c80b8"
integrity sha512-oGZRv2OT1lO2UF1zUcwdTb3wqUwI0kBGTgt/T7OdSj6M6N5m3o5uPf0AIW6lVxGGoiWUR7e2AwTE+xiwK8WQig==
mz@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32"
integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
dependencies:
any-promise "^1.0.0"
object-assign "^4.0.1"
thenify-all "^1.0.0"
nanoid@^3.3.7:
version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
object-assign@^4.0.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
object-hash@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9"
integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
path-key@^3.1.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375"
integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
path-parse@^1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-scurry@^1.10.2:
version "1.10.2"
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.2.tgz#8f6357eb1239d5fa1da8b9f70e9c080675458ba7"
integrity sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==
dependencies:
lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
picocolors@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
pify@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
pirates@^4.0.1:
version "4.0.6"
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9"
integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==
postcss-import@^15.1.0:
version "15.1.0"
resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70"
integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==
dependencies:
postcss-value-parser "^4.0.0"
read-cache "^1.0.0"
resolve "^1.1.7"
postcss-js@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2"
integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==
dependencies:
camelcase-css "^2.0.1"
postcss-load-config@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3"
integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==
dependencies:
lilconfig "^3.0.0"
yaml "^2.3.4"
postcss-nested@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c"
integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==
dependencies:
postcss-selector-parser "^6.0.11"
postcss-selector-parser@^6.0.11:
version "6.0.16"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz#3b88b9f5c5abd989ef4e2fc9ec8eedd34b20fb04"
integrity sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==
dependencies:
cssesc "^3.0.0"
util-deprecate "^1.0.2"
postcss-value-parser@^4.0.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
postcss@^8.4.23:
version "8.4.38"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
dependencies:
nanoid "^3.3.7"
picocolors "^1.0.0"
source-map-js "^1.2.0"
queue-microtask@^1.2.2:
version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
read-cache@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==
dependencies:
pify "^2.3.0"
readdirp@~3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
dependencies:
picomatch "^2.2.1"
resolve@^1.1.7, resolve@^1.22.2:
version "1.22.8"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d"
integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==
dependencies:
is-core-module "^2.13.0"
path-parse "^1.0.7"
supports-preserve-symlinks-flag "^1.0.0"
reusify@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
run-parallel@^1.1.9:
version "1.2.0"
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
dependencies:
queue-microtask "^1.2.2"
shebang-command@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea"
integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
dependencies:
shebang-regex "^3.0.0"
shebang-regex@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
signal-exit@^4.0.1:
version "4.1.0"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
source-map-js@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^5.0.1, string-width@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
dependencies:
eastasianwidth "^0.2.0"
emoji-regex "^9.2.2"
strip-ansi "^7.0.1"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^7.0.1:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
dependencies:
ansi-regex "^6.0.1"
sucrase@^3.32.0:
version "3.35.0"
resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263"
integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==
dependencies:
"@jridgewell/gen-mapping" "^0.3.2"
commander "^4.0.0"
glob "^10.3.10"
lines-and-columns "^1.1.6"
mz "^2.7.0"
pirates "^4.0.1"
ts-interface-checker "^0.1.9"
supports-preserve-symlinks-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
tailwindcss@^3.4.3:
version "3.4.3"
resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.3.tgz#be48f5283df77dfced705451319a5dffb8621519"
integrity sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==
dependencies:
"@alloc/quick-lru" "^5.2.0"
arg "^5.0.2"
chokidar "^3.5.3"
didyoumean "^1.2.2"
dlv "^1.1.3"
fast-glob "^3.3.0"
glob-parent "^6.0.2"
is-glob "^4.0.3"
jiti "^1.21.0"
lilconfig "^2.1.0"
micromatch "^4.0.5"
normalize-path "^3.0.0"
object-hash "^3.0.0"
picocolors "^1.0.0"
postcss "^8.4.23"
postcss-import "^15.1.0"
postcss-js "^4.0.1"
postcss-load-config "^4.0.1"
postcss-nested "^6.0.1"
postcss-selector-parser "^6.0.11"
resolve "^1.22.2"
sucrase "^3.32.0"
thenify-all@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726"
integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==
dependencies:
thenify ">= 3.1.0 < 4"
"thenify@>= 3.1.0 < 4":
version "3.3.1"
resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f"
integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==
dependencies:
any-promise "^1.0.0"
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
ts-interface-checker@^0.1.9:
version "0.1.13"
resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
util-deprecate@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
dependencies:
isexe "^2.0.0"
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"
integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
dependencies:
ansi-styles "^6.1.0"
string-width "^5.0.1"
strip-ansi "^7.0.1"
yaml@^2.3.4:
version "2.4.2"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.2.tgz#7a2b30f2243a5fc299e1f14ca58d475ed4bc5362"
integrity sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==