├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── README.md ├── apps ├── server │ ├── .air.toml │ ├── Dockerfile │ ├── cmd │ │ └── goat │ │ │ └── main.go │ ├── data │ │ └── readme.md │ ├── etc │ │ └── litestream.yml │ ├── go.mod │ ├── go.sum │ ├── internal │ │ ├── db │ │ │ └── db.go │ │ └── goat │ │ │ └── handler.go │ ├── justfile │ ├── package.json │ ├── proto │ │ └── goat │ │ │ └── v1 │ │ │ ├── goat.pb.go │ │ │ └── goatv1connect │ │ │ └── goat.connect.go │ └── scripts │ │ └── run.sh └── web │ ├── .env │ ├── .gitignore │ ├── .vscode │ └── settings.json │ ├── Dockerfile │ ├── README.md │ ├── index.html │ ├── nginx │ └── nginx.conf │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt │ ├── src │ ├── gen │ │ └── proto │ │ │ └── goat │ │ │ └── v1 │ │ │ ├── goat-GoatService_connectquery.ts │ │ │ └── goat_pb.ts │ ├── logo.svg │ ├── main.tsx │ ├── reportWebVitals.ts │ ├── routeTree.gen.ts │ ├── routes │ │ ├── __root.tsx │ │ ├── index.tsx │ │ └── results.tsx │ └── styles.css │ ├── tsconfig.json │ └── vite.config.js ├── goat.png ├── justfile ├── package.json ├── packages ├── proto │ ├── buf.gen.yaml │ ├── goat │ │ └── v1 │ │ │ └── goat.proto │ ├── justfile │ └── package.json ├── typescript-config │ ├── base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json └── ui │ ├── eslint.config.mjs │ ├── package.json │ ├── postcss.config.js │ ├── src │ ├── components │ │ ├── button.tsx │ │ ├── card.tsx │ │ └── charts.tsx │ ├── lib │ │ └── utils.ts │ └── styles.css │ ├── tailwind.config.ts │ └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── turbo.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | .swc/ 16 | 17 | # misc 18 | .DS_Store 19 | *.pem 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | # turbo 33 | .turbo 34 | 35 | # ui 36 | dist/ 37 | 38 | apps/server/tmp -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | auto-install-peers = true 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | { 4 | "mode": "auto" 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Goat Stack 3 |

4 | 5 | 6 | # GoaT Stack (Go and Typescript) 7 | 8 | ## What is the Goat Stack? 9 | 10 | It's a React SPA with a Golang backend. The frontend and the backend are fully wired with Protocol Buffer. 11 | 12 | ## Requirements 13 | - [Just](https://just.systems) 14 | - [Golang](https://go.dev/) 15 | - [pnpm](https://pnpm.io) 16 | - [Node](https://nodejs.org/en) 17 | 18 | ## The Stack 19 | 20 | ### Backend 21 | 22 | - [Chi](https://go-chi.io/#/) 23 | - [ConnectRPC](https://connectrpc.com/) 24 | - [SQLite](https://www.sqlite.org/) 25 | 26 | 27 | ### Frontend 28 | 29 | - [React](https://react.dev/) 30 | - [Vite](https://vite.dev/) 31 | - [Tanstack Router](https://tanstack.com/router/latest) 32 | - [Tanstack Query](https://tanstack.com/query/latest) 33 | - [ConnectRPC](https://connectrpc.com/) 34 | - [Tailwind CSS](https://www.tailwindcss.com) 35 | 36 | ## How to get started 🚀 37 | 38 | 1. Init the project 39 | ```bash 40 | just init 41 | ``` 42 | 43 | 2. Launch the dev 44 | 45 | ```bash 46 | just dev 47 | ``` 48 | 49 | ## Todo 50 | 51 | - [X] Docker for Server 52 | - [X] Docker for React app 53 | - [X] Add Database SQLite 54 | - [X] Add LiteStream 55 | - [ ] Improve DX with Just 56 | -------------------------------------------------------------------------------- /apps/server/.air.toml: -------------------------------------------------------------------------------- 1 | #:schema ./foo-schema.json 2 | root = "." 3 | testdata_dir = "testdata" 4 | tmp_dir = "tmp" 5 | 6 | [build] 7 | args_bin = [] 8 | bin = "./tmp/main" 9 | cmd = "go build -o ./tmp/main ./cmd/goat/main.go" 10 | delay = 1000 11 | exclude_dir = ["assets", "tmp", "vendor", "testdata"] 12 | exclude_file = [] 13 | exclude_regex = ["_test.go"] 14 | exclude_unchanged = false 15 | follow_symlink = false 16 | full_bin = "" 17 | include_dir = [] 18 | include_ext = ["go", "tpl", "tmpl", "html"] 19 | include_file = [] 20 | kill_delay = "0s" 21 | log = "build-errors.log" 22 | poll = false 23 | poll_interval = 0 24 | post_cmd = [] 25 | pre_cmd = [] 26 | rerun = false 27 | rerun_delay = 500 28 | send_interrupt = false 29 | stop_on_error = false 30 | 31 | [color] 32 | app = "" 33 | build = "yellow" 34 | main = "magenta" 35 | runner = "green" 36 | watcher = "cyan" 37 | 38 | [log] 39 | main_only = false 40 | silent = false 41 | time = false 42 | 43 | [misc] 44 | clean_on_exit = false 45 | 46 | [proxy] 47 | app_port = 0 48 | enabled = false 49 | proxy_port = 0 50 | 51 | [screen] 52 | clear_on_rebuild = false 53 | keep_scroll = true 54 | -------------------------------------------------------------------------------- /apps/server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24-alpine AS builder 2 | 3 | WORKDIR /go/src/app 4 | 5 | RUN apk add --no-cache tzdata 6 | ENV TZ=UTC 7 | 8 | ENV CGO_ENABLED=0 9 | ENV GOOS=linux 10 | ENV GOARCH=amd64 11 | 12 | COPY go.mod . 13 | COPY go.sum . 14 | RUN go mod download 15 | 16 | COPY . . 17 | RUN go build -trimpath -ldflags "-s -w" -o goat ./cmd/goat 18 | 19 | 20 | 21 | FROM alpine 22 | 23 | RUN apk add bash 24 | 25 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 26 | COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo 27 | COPY --from=builder /go/src/app/goat /opt/bin/goat 28 | COPY --from=litestream/litestream:latest /usr/local/bin/litestream /usr/local/bin/litestream 29 | 30 | 31 | RUN mkdir -p /data 32 | # WORKDIR /opt/bin 33 | 34 | EXPOSE 8080 35 | COPY scripts/run.sh /scripts/run.sh 36 | COPY etc/litestream.yml /etc/litestream.yml 37 | 38 | 39 | ENV TZ=UTC 40 | ENV USER=1000 41 | 42 | ENTRYPOINT "/scripts/run.sh" 43 | -------------------------------------------------------------------------------- /apps/server/cmd/goat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | goat_handler "goat/internal/goat" 6 | "log" 7 | "net/http" 8 | "os" 9 | "os/signal" 10 | "syscall" 11 | "time" 12 | 13 | connectcors "connectrpc.com/cors" 14 | 15 | "github.com/go-chi/chi/v5" 16 | "github.com/go-chi/chi/v5/middleware" 17 | "github.com/go-chi/cors" 18 | ) 19 | 20 | func main() { 21 | r := chi.NewRouter() 22 | r.Use(cors.Handler(cors.Options{ 23 | AllowedOrigins: []string{"*"}, 24 | AllowedMethods: connectcors.AllowedMethods(), 25 | AllowedHeaders: connectcors.AllowedHeaders(), 26 | ExposedHeaders: connectcors.ExposedHeaders(), 27 | })) 28 | r.Use(middleware.Logger) 29 | 30 | r.Get("/", func(w http.ResponseWriter, r *http.Request) { 31 | w.Write([]byte("This is our server")) 32 | }) 33 | 34 | r.Get("/health", func(w http.ResponseWriter, r *http.Request) { 35 | w.Write([]byte("Healty")) 36 | }) 37 | 38 | goat_handler.RegisterConnect(r) 39 | 40 | // Start our server 41 | server := newServer(":"+"8080", r) 42 | 43 | // Server run context 44 | serverCtx, serverStopCtx := context.WithCancel(context.Background()) 45 | 46 | // Listen for syscall signals for process to interrupt/quit 47 | sig := make(chan os.Signal, 1) 48 | signal.Notify(sig, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) 49 | 50 | go func() { 51 | <-sig 52 | 53 | // Shutdown signal with grace period of 30 seconds 54 | shutdownCtx, cancel := context.WithTimeout(serverCtx, 30*time.Second) 55 | defer cancel() 56 | go func() { 57 | <-shutdownCtx.Done() 58 | if shutdownCtx.Err() == context.DeadlineExceeded { 59 | log.Fatal("graceful shutdown timed out.. forcing exit.") 60 | } 61 | }() 62 | 63 | // Trigger graceful shutdown 64 | err := server.Shutdown(shutdownCtx) 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | serverStopCtx() 69 | }() 70 | 71 | // Run the server 72 | err := server.ListenAndServe() 73 | if err != nil && err != http.ErrServerClosed { 74 | log.Fatal(err) 75 | } 76 | 77 | // Wait for server context to be stopped 78 | <-serverCtx.Done() 79 | } 80 | 81 | func newServer(addr string, r *chi.Mux) *http.Server { 82 | return &http.Server{ 83 | Addr: addr, 84 | Handler: r, 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /apps/server/data/readme.md: -------------------------------------------------------------------------------- 1 | Your Database will be here 2 | -------------------------------------------------------------------------------- /apps/server/etc/litestream.yml: -------------------------------------------------------------------------------- 1 | dbs: 2 | - path: /data/db 3 | replicas: 4 | - type: s3 5 | endpoint: https://${CLOUDFLARE_R2_ACCOUNT_ID}.r2.cloudflarestorage.com/ 6 | bucket: goat-stack 7 | access-key-id: ${CLOUDFLARE_R2_ACCESS_KEY_ID} 8 | secret-access-key: ${CLOUDFLARE_R2_SECRET_ACCESS_KEY} 9 | -------------------------------------------------------------------------------- /apps/server/go.mod: -------------------------------------------------------------------------------- 1 | module goat 2 | 3 | go 1.24.1 4 | 5 | require ( 6 | connectrpc.com/connect v1.18.1 7 | connectrpc.com/cors v0.1.0 8 | github.com/go-chi/chi/v5 v5.2.1 9 | github.com/go-chi/cors v1.2.1 10 | github.com/jmoiron/sqlx v1.4.0 11 | github.com/rs/zerolog v1.33.0 12 | google.golang.org/protobuf v1.36.5 13 | modernc.org/sqlite v1.36.1 14 | ) 15 | 16 | require ( 17 | dario.cat/mergo v1.0.1 // indirect 18 | github.com/air-verse/air v1.61.7 // indirect 19 | github.com/bep/godartsass v1.2.0 // indirect 20 | github.com/bep/godartsass/v2 v2.1.0 // indirect 21 | github.com/bep/golibsass v1.2.0 // indirect 22 | github.com/cli/safeexec v1.0.1 // indirect 23 | github.com/creack/pty v1.1.23 // indirect 24 | github.com/dustin/go-humanize v1.0.1 // indirect 25 | github.com/fatih/color v1.17.0 // indirect 26 | github.com/fsnotify/fsnotify v1.7.0 // indirect 27 | github.com/gobwas/glob v0.2.3 // indirect 28 | github.com/gohugoio/hugo v0.134.3 // indirect 29 | github.com/google/uuid v1.6.0 // indirect 30 | github.com/mattn/go-colorable v0.1.13 // indirect 31 | github.com/mattn/go-isatty v0.0.20 // indirect 32 | github.com/ncruces/go-strftime v0.1.9 // indirect 33 | github.com/pelletier/go-toml v1.9.5 // indirect 34 | github.com/pelletier/go-toml/v2 v2.2.3 // indirect 35 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect 36 | github.com/spf13/afero v1.11.0 // indirect 37 | github.com/spf13/cast v1.7.0 // indirect 38 | github.com/tdewolff/parse/v2 v2.7.15 // indirect 39 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect 40 | golang.org/x/sys v0.30.0 // indirect 41 | golang.org/x/text v0.18.0 // indirect 42 | modernc.org/libc v1.61.13 // indirect 43 | modernc.org/mathutil v1.7.1 // indirect 44 | modernc.org/memory v1.8.2 // indirect 45 | ) 46 | 47 | tool github.com/air-verse/air 48 | -------------------------------------------------------------------------------- /apps/server/go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= 3 | connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= 4 | connectrpc.com/cors v0.1.0 h1:f3gTXJyDZPrDIZCQ567jxfD9PAIpopHiRDnJRt3QuOQ= 5 | connectrpc.com/cors v0.1.0/go.mod h1:v8SJZCPfHtGH1zsm+Ttajpozd4cYIUryl4dFB6QEpfg= 6 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 7 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 8 | filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= 9 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 10 | github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU= 11 | github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg= 12 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 13 | github.com/air-verse/air v1.61.7 h1:MtOZs6wYoYYXm+S4e+ORjkq9BjvyEamKJsHcvko8LrQ= 14 | github.com/air-verse/air v1.61.7/go.mod h1:QW4HkIASdtSnwaYof1zgJCSxd41ebvix10t5ubtm9cg= 15 | github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E= 16 | github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I= 17 | github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M= 18 | github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= 19 | github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps= 20 | github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU= 21 | github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY= 22 | github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= 23 | github.com/bep/gitmap v1.6.0 h1:sDuQMm9HoTL0LtlrfxjbjgAg2wHQd4nkMup2FInYzhA= 24 | github.com/bep/gitmap v1.6.0/go.mod h1:n+3W1f/rot2hynsqEGxGMErPRgT41n9CkGuzPvz9cIw= 25 | github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA= 26 | github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c= 27 | github.com/bep/godartsass v1.2.0 h1:E2VvQrxAHAFwbjyOIExAMmogTItSKodoKuijNrGm5yU= 28 | github.com/bep/godartsass v1.2.0/go.mod h1:6LvK9RftsXMxGfsA0LDV12AGc4Jylnu6NgHL+Q5/pE8= 29 | github.com/bep/godartsass/v2 v2.1.0 h1:fq5Y1xYf4diu4tXABiekZUCA+5l/dmNjGKCeQwdy+s0= 30 | github.com/bep/godartsass/v2 v2.1.0/go.mod h1:AcP8QgC+OwOXEq6im0WgDRYK7scDsmZCEW62o1prQLo= 31 | github.com/bep/golibsass v1.2.0 h1:nyZUkKP/0psr8nT6GR2cnmt99xS93Ji82ZD9AgOK6VI= 32 | github.com/bep/golibsass v1.2.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA= 33 | github.com/bep/gowebp v0.4.0 h1:QihuVnvIKbRoeBNQkN0JPMM8ClLmD6V2jMftTFwSK3Q= 34 | github.com/bep/gowebp v0.4.0/go.mod h1:95gtYkAA8iIn1t3HkAPurRCVGV/6NhgaHJ1urz0iIwc= 35 | github.com/bep/imagemeta v0.8.1 h1:tjZLPRftjxU7PTI87o5e5WKOFQ4S9S0engiP1OTpJTI= 36 | github.com/bep/imagemeta v0.8.1/go.mod h1:5piPAq5Qomh07m/dPPCLN3mDJyFusvUG7VwdRD/vX0s= 37 | github.com/bep/lazycache v0.5.0 h1:9FJRrEp/s3BUpGEfTvLhmv50N4dXzoZnyRPU6NOUv0w= 38 | github.com/bep/lazycache v0.5.0/go.mod h1:NmRm7Dexh3pmR1EignYR8PjO2cWybFQ68+QgY3VMCSc= 39 | github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ= 40 | github.com/bep/logg v0.4.0/go.mod h1:Ccp9yP3wbR1mm++Kpxet91hAZBEQgmWgFgnXX3GkIV0= 41 | github.com/bep/overlayfs v0.9.2 h1:qJEmFInsW12L7WW7dOTUhnMfyk/fN9OCDEO5Gr8HSDs= 42 | github.com/bep/overlayfs v0.9.2/go.mod h1:aYY9W7aXQsGcA7V9x/pzeR8LjEgIxbtisZm8Q7zPz40= 43 | github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI= 44 | github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0= 45 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 46 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 47 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 48 | github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME= 49 | github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= 50 | github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= 51 | github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= 52 | github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= 53 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 54 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 55 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 56 | github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= 57 | github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 58 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 59 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 60 | github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc= 61 | github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI= 62 | github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= 63 | github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 64 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 65 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 66 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 67 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 68 | github.com/evanw/esbuild v0.23.1 h1:ociewhY6arjTarKLdrXfDTgy25oxhTZmzP8pfuBTfTA= 69 | github.com/evanw/esbuild v0.23.1/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= 70 | github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 71 | github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 72 | github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o= 73 | github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= 74 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 75 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 76 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 77 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 78 | github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY= 79 | github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM= 80 | github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= 81 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 82 | github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= 83 | github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 84 | github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= 85 | github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= 86 | github.com/go-openapi/jsonpointer v0.20.2 h1:mQc3nmndL8ZBzStEo3JYF8wzmeWffDH4VbXz58sAx6Q= 87 | github.com/go-openapi/jsonpointer v0.20.2/go.mod h1:bHen+N0u1KEO3YlmqOjTT9Adn1RfD91Ar825/PuiRVs= 88 | github.com/go-openapi/swag v0.22.8 h1:/9RjDSQ0vbFR+NyjGMkFTsA1IA0fmhKSThmfGZjicbw= 89 | github.com/go-openapi/swag v0.22.8/go.mod h1:6QT22icPLEqAM/z/TChgb4WAveCHF92+2gF0CNjHpPI= 90 | github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= 91 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 92 | github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= 93 | github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= 94 | github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= 95 | github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= 96 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 97 | github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e h1:QArsSubW7eDh8APMXkByjQWvuljwPGAGQpJEFn0F0wY= 98 | github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e/go.mod h1:3Ltoo9Banwq0gOtcOwxuHG6omk+AwsQPADyw2vQYOJQ= 99 | github.com/gohugoio/hashstructure v0.1.0 h1:kBSTMLMyTXbrJVAxaKI+wv30MMJJxn9Q8kfQtJaZ400= 100 | github.com/gohugoio/hashstructure v0.1.0/go.mod h1:8ohPTAfQLTs2WdzB6k9etmQYclDUeNsIHGPAFejbsEA= 101 | github.com/gohugoio/httpcache v0.7.0 h1:ukPnn04Rgvx48JIinZvZetBfHaWE7I01JR2Q2RrQ3Vs= 102 | github.com/gohugoio/httpcache v0.7.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI= 103 | github.com/gohugoio/hugo v0.134.3 h1:Pn2KECXAAQWCd2uryDcmtzVhNJWGF5Pt6CplQvLcWe0= 104 | github.com/gohugoio/hugo v0.134.3/go.mod h1:/1gnGxlWfAzQarxcQ+tMvKw4e/IMBwy0DFbRxORwOtY= 105 | github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0 h1:MNdY6hYCTQEekY0oAfsxWZU1CDt6iH+tMLgyMJQh/sg= 106 | github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0/go.mod h1:oBdBVuiZ0fv9xd8xflUgt53QxW5jOCb1S+xntcN4SKo= 107 | github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.0 h1:7PY5PIJ2mck7v6R52yCFvvYHvsPMEbulgRviw3I9lP4= 108 | github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.0/go.mod h1:r8g5S7bHfdj0+9ShBog864ufCsVODKQZNjYYY8OnJpM= 109 | github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc= 110 | github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4= 111 | github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo= 112 | github.com/gohugoio/localescompressed v1.0.1/go.mod h1:jBF6q8D7a0vaEmcWPNcAjUZLJaIVNiwvM3WlmTvooB0= 113 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 114 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 115 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 116 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 117 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 118 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 119 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 120 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 121 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 122 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 123 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 124 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 125 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 126 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 127 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 128 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 129 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 130 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 131 | github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= 132 | github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= 133 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 134 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 135 | github.com/hairyhenderson/go-codeowners v0.5.0 h1:dpQB+hVHiRc2VVvc2BHxkuM+tmu9Qej/as3apqUbsWc= 136 | github.com/hairyhenderson/go-codeowners v0.5.0/go.mod h1:R3uW1OQXEj2Gu6/OvZ7bt6hr0qdkLvUWPiqNaWnexpo= 137 | github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 138 | github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 139 | github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= 140 | github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= 141 | github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU= 142 | github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA= 143 | github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= 144 | github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 145 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 146 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 147 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 148 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 149 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 150 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 151 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 152 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 153 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 154 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 155 | github.com/kyokomi/emoji/v2 v2.2.13 h1:GhTfQa67venUUvmleTNFnb+bi7S3aocF7ZCXU9fSO7U= 156 | github.com/kyokomi/emoji/v2 v2.2.13/go.mod h1:JUcn42DTdsXJo1SWanHh4HKDEyPaR5CqkmoirZZP9qE= 157 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 158 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 159 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 160 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 161 | github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1rSnAZns+1msaCXetrMFE= 162 | github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc= 163 | github.com/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mrXGUqIwn0= 164 | github.com/marekm4/color-extractor v1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA= 165 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 166 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 167 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 168 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 169 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 170 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 171 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 172 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 173 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 174 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 175 | github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= 176 | github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 177 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= 178 | github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= 179 | github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc= 180 | github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI= 181 | github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= 182 | github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 183 | github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek= 184 | github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o= 185 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 186 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 187 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= 188 | github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= 189 | github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= 190 | github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 191 | github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= 192 | github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= 193 | github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= 194 | github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= 195 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 196 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 197 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 198 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 199 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 200 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= 201 | github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= 202 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 203 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 204 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 205 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 206 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 207 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 208 | github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= 209 | github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 210 | github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 211 | github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 212 | github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= 213 | github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 214 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 215 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 216 | github.com/tdewolff/minify/v2 v2.20.37 h1:Q97cx4STXCh1dlWDlNHZniE8BJ2EBL0+2b0n92BJQhw= 217 | github.com/tdewolff/minify/v2 v2.20.37/go.mod h1:L1VYef/jwKw6Wwyk5A+T0mBjjn3mMPgmjjA688RNsxU= 218 | github.com/tdewolff/parse/v2 v2.7.15 h1:hysDXtdGZIRF5UZXwpfn3ZWRbm+ru4l53/ajBRGpCTw= 219 | github.com/tdewolff/parse/v2 v2.7.15/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= 220 | github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= 221 | github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo= 222 | github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8= 223 | github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g= 224 | github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs= 225 | github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg= 226 | github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= 227 | github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4= 228 | github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U= 229 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 230 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 231 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= 232 | golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= 233 | golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= 234 | golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= 235 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 236 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 237 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 238 | golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= 239 | golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 240 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 241 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 242 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 243 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 244 | golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 245 | golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 246 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 247 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 248 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 249 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 250 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 251 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 252 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 253 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 254 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 255 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 256 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 257 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 258 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 259 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 260 | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= 261 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 262 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 263 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 264 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 265 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 266 | golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= 267 | golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= 268 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 269 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 270 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 271 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 272 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 273 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 274 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 275 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 276 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 277 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 278 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 279 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 280 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 281 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 282 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 283 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 284 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 285 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 286 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 287 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 288 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 289 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 290 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 291 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 292 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 293 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 294 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 295 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 296 | modernc.org/cc/v4 v4.24.4 h1:TFkx1s6dCkQpd6dKurBNmpo+G8Zl4Sq/ztJ+2+DEsh0= 297 | modernc.org/cc/v4 v4.24.4/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= 298 | modernc.org/ccgo/v4 v4.23.16 h1:Z2N+kk38b7SfySC1ZkpGLN2vthNJP1+ZzGZIlH7uBxo= 299 | modernc.org/ccgo/v4 v4.23.16/go.mod h1:nNma8goMTY7aQZQNTyN9AIoJfxav4nvTnvKThAeMDdo= 300 | modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= 301 | modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= 302 | modernc.org/gc/v2 v2.6.3 h1:aJVhcqAte49LF+mGveZ5KPlsp4tdGdAOT4sipJXADjw= 303 | modernc.org/gc/v2 v2.6.3/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= 304 | modernc.org/libc v1.61.13 h1:3LRd6ZO1ezsFiX1y+bHd1ipyEHIJKvuprv0sLTBwLW8= 305 | modernc.org/libc v1.61.13/go.mod h1:8F/uJWL/3nNil0Lgt1Dpz+GgkApWh04N3el3hxJcA6E= 306 | modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= 307 | modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= 308 | modernc.org/memory v1.8.2 h1:cL9L4bcoAObu4NkxOlKWBWtNHIsnnACGF/TbqQ6sbcI= 309 | modernc.org/memory v1.8.2/go.mod h1:ZbjSvMO5NQ1A2i3bWeDiVMxIorXwdClKE/0SZ+BMotU= 310 | modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= 311 | modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= 312 | modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= 313 | modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= 314 | modernc.org/sqlite v1.36.1 h1:bDa8BJUH4lg6EGkLbahKe/8QqoF8p9gArSc6fTqYhyQ= 315 | modernc.org/sqlite v1.36.1/go.mod h1:7MPwH7Z6bREicF9ZVUR78P1IKuxfZ8mRIDHD0iD+8TU= 316 | modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= 317 | modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= 318 | modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= 319 | modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= 320 | -------------------------------------------------------------------------------- /apps/server/internal/db/db.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | 5 | "github.com/rs/zerolog/log" 6 | _ "modernc.org/sqlite" 7 | "github.com/jmoiron/sqlx" 8 | ) 9 | 10 | 11 | var schema = ` 12 | create table if not exists vote ( 13 | id integer primary key, 14 | timestamp integer, 15 | vote text not null 16 | ); 17 | ` 18 | 19 | func New() *sqlx.DB { 20 | 21 | db, err := sqlx.Open("sqlite", "file:./data/db") 22 | 23 | if err != nil { 24 | log.Fatal().Err(err).Msg("failed to open database") 25 | return nil 26 | } 27 | 28 | if _, err = db.Exec(schema); err != nil { 29 | return nil 30 | } 31 | return db 32 | 33 | } 34 | -------------------------------------------------------------------------------- /apps/server/internal/goat/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "goat/internal/db" 7 | goatv1 "goat/proto/goat/v1" 8 | "goat/proto/goat/v1/goatv1connect" 9 | "time" 10 | 11 | "connectrpc.com/connect" 12 | "github.com/go-chi/chi/v5" 13 | "github.com/jmoiron/sqlx" 14 | ) 15 | 16 | type goatHandler struct { 17 | db *sqlx.DB 18 | } 19 | 20 | type VoteCount struct { 21 | Count int64 `db:"count"` 22 | } 23 | 24 | func NewGoatHandler() *goatHandler { 25 | return &goatHandler{ 26 | db: db.New(), 27 | } 28 | } 29 | 30 | func (h *goatHandler) Vote(ctx context.Context, req *connect.Request[goatv1.VoteRequest]) (*connect.Response[goatv1.VoteResponse], error) { 31 | 32 | tx := h.db.MustBegin() 33 | 34 | var value string 35 | switch req.Msg.Vote { 36 | case goatv1.Vote_YES: 37 | value = "yes" 38 | break 39 | case goatv1.Vote_NO: 40 | value = "no" 41 | break 42 | 43 | default: 44 | break 45 | } 46 | r := tx.MustExec("INSERT INTO vote (timestamp, vote) VALUES ($1, $2)", time.Now().Unix(), value) 47 | fmt.Println(r.LastInsertId()) 48 | tx.Commit() 49 | res := connect.NewResponse(&goatv1.VoteResponse{ 50 | Success: true, 51 | }) 52 | 53 | return res, nil 54 | } 55 | 56 | func (h *goatHandler) GetVotes(context.Context, *connect.Request[goatv1.GetVotesRequest]) (*connect.Response[goatv1.GetVotesResponse], error) { 57 | 58 | totalCount := VoteCount{} 59 | error := h.db.Get(&totalCount, "SELECT count(*) as count FROM vote") 60 | if error != nil { 61 | return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to get votes")) 62 | } 63 | yesCount := VoteCount{} 64 | error = h.db.Get(&yesCount, "SELECT count(*) as count FROM vote where vote = 'yes'") 65 | if error != nil { 66 | return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to get votes")) 67 | } 68 | fmt.Println("yes", yesCount.Count) 69 | noCount := VoteCount{} 70 | error = h.db.Get(&noCount, "SELECT count(*) as count FROM vote where vote = 'no'") 71 | if error != nil { 72 | return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to get votes")) 73 | } 74 | maybeCount := VoteCount{} 75 | error = h.db.Get(&maybeCount, "SELECT count(*) as count FROM vote where vote = 'maybe'") 76 | if error != nil { 77 | return nil, connect.NewError(connect.CodeInternal, fmt.Errorf("failed to get votes")) 78 | } 79 | 80 | res := connect.NewResponse(&goatv1.GetVotesResponse{ 81 | Yes: yesCount.Count, 82 | No: noCount.Count, 83 | }) 84 | return res, nil 85 | } 86 | 87 | func RegisterConnect(r *chi.Mux) { 88 | goatServer := NewGoatHandler() 89 | 90 | path, handler := goatv1connect.NewGoatServiceHandler(goatServer) 91 | 92 | r.Group(func(r chi.Router) { 93 | r.Mount(path, handler) 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /apps/server/justfile: -------------------------------------------------------------------------------- 1 | build: 2 | go build -o ./tmp/main ./cmd/goat/main.go 3 | 4 | update: 5 | go mod tidy 6 | go get -u all 7 | go mod tidy 8 | -------------------------------------------------------------------------------- /apps/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@goat/server", 3 | "private": true, 4 | "scripts": { 5 | "dev":"air" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/server/proto/goat/v1/goat.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.36.5 4 | // protoc (unknown) 5 | // source: goat/v1/goat.proto 6 | 7 | package goatv1 8 | 9 | import ( 10 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 11 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 12 | reflect "reflect" 13 | sync "sync" 14 | unsafe "unsafe" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | type Vote int32 25 | 26 | const ( 27 | Vote_YES Vote = 0 28 | Vote_NO Vote = 1 29 | ) 30 | 31 | // Enum value maps for Vote. 32 | var ( 33 | Vote_name = map[int32]string{ 34 | 0: "YES", 35 | 1: "NO", 36 | } 37 | Vote_value = map[string]int32{ 38 | "YES": 0, 39 | "NO": 1, 40 | } 41 | ) 42 | 43 | func (x Vote) Enum() *Vote { 44 | p := new(Vote) 45 | *p = x 46 | return p 47 | } 48 | 49 | func (x Vote) String() string { 50 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 51 | } 52 | 53 | func (Vote) Descriptor() protoreflect.EnumDescriptor { 54 | return file_goat_v1_goat_proto_enumTypes[0].Descriptor() 55 | } 56 | 57 | func (Vote) Type() protoreflect.EnumType { 58 | return &file_goat_v1_goat_proto_enumTypes[0] 59 | } 60 | 61 | func (x Vote) Number() protoreflect.EnumNumber { 62 | return protoreflect.EnumNumber(x) 63 | } 64 | 65 | // Deprecated: Use Vote.Descriptor instead. 66 | func (Vote) EnumDescriptor() ([]byte, []int) { 67 | return file_goat_v1_goat_proto_rawDescGZIP(), []int{0} 68 | } 69 | 70 | type VoteRequest struct { 71 | state protoimpl.MessageState `protogen:"open.v1"` 72 | Vote Vote `protobuf:"varint,1,opt,name=Vote,proto3,enum=goat.v1.Vote" json:"Vote,omitempty"` 73 | unknownFields protoimpl.UnknownFields 74 | sizeCache protoimpl.SizeCache 75 | } 76 | 77 | func (x *VoteRequest) Reset() { 78 | *x = VoteRequest{} 79 | mi := &file_goat_v1_goat_proto_msgTypes[0] 80 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 81 | ms.StoreMessageInfo(mi) 82 | } 83 | 84 | func (x *VoteRequest) String() string { 85 | return protoimpl.X.MessageStringOf(x) 86 | } 87 | 88 | func (*VoteRequest) ProtoMessage() {} 89 | 90 | func (x *VoteRequest) ProtoReflect() protoreflect.Message { 91 | mi := &file_goat_v1_goat_proto_msgTypes[0] 92 | if x != nil { 93 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 94 | if ms.LoadMessageInfo() == nil { 95 | ms.StoreMessageInfo(mi) 96 | } 97 | return ms 98 | } 99 | return mi.MessageOf(x) 100 | } 101 | 102 | // Deprecated: Use VoteRequest.ProtoReflect.Descriptor instead. 103 | func (*VoteRequest) Descriptor() ([]byte, []int) { 104 | return file_goat_v1_goat_proto_rawDescGZIP(), []int{0} 105 | } 106 | 107 | func (x *VoteRequest) GetVote() Vote { 108 | if x != nil { 109 | return x.Vote 110 | } 111 | return Vote_YES 112 | } 113 | 114 | type GetVotesRequest struct { 115 | state protoimpl.MessageState `protogen:"open.v1"` 116 | unknownFields protoimpl.UnknownFields 117 | sizeCache protoimpl.SizeCache 118 | } 119 | 120 | func (x *GetVotesRequest) Reset() { 121 | *x = GetVotesRequest{} 122 | mi := &file_goat_v1_goat_proto_msgTypes[1] 123 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 124 | ms.StoreMessageInfo(mi) 125 | } 126 | 127 | func (x *GetVotesRequest) String() string { 128 | return protoimpl.X.MessageStringOf(x) 129 | } 130 | 131 | func (*GetVotesRequest) ProtoMessage() {} 132 | 133 | func (x *GetVotesRequest) ProtoReflect() protoreflect.Message { 134 | mi := &file_goat_v1_goat_proto_msgTypes[1] 135 | if x != nil { 136 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 137 | if ms.LoadMessageInfo() == nil { 138 | ms.StoreMessageInfo(mi) 139 | } 140 | return ms 141 | } 142 | return mi.MessageOf(x) 143 | } 144 | 145 | // Deprecated: Use GetVotesRequest.ProtoReflect.Descriptor instead. 146 | func (*GetVotesRequest) Descriptor() ([]byte, []int) { 147 | return file_goat_v1_goat_proto_rawDescGZIP(), []int{1} 148 | } 149 | 150 | type GetVotesResponse struct { 151 | state protoimpl.MessageState `protogen:"open.v1"` 152 | Yes int64 `protobuf:"varint,1,opt,name=Yes,proto3" json:"Yes,omitempty"` 153 | No int64 `protobuf:"varint,2,opt,name=No,proto3" json:"No,omitempty"` 154 | unknownFields protoimpl.UnknownFields 155 | sizeCache protoimpl.SizeCache 156 | } 157 | 158 | func (x *GetVotesResponse) Reset() { 159 | *x = GetVotesResponse{} 160 | mi := &file_goat_v1_goat_proto_msgTypes[2] 161 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 162 | ms.StoreMessageInfo(mi) 163 | } 164 | 165 | func (x *GetVotesResponse) String() string { 166 | return protoimpl.X.MessageStringOf(x) 167 | } 168 | 169 | func (*GetVotesResponse) ProtoMessage() {} 170 | 171 | func (x *GetVotesResponse) ProtoReflect() protoreflect.Message { 172 | mi := &file_goat_v1_goat_proto_msgTypes[2] 173 | if x != nil { 174 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 175 | if ms.LoadMessageInfo() == nil { 176 | ms.StoreMessageInfo(mi) 177 | } 178 | return ms 179 | } 180 | return mi.MessageOf(x) 181 | } 182 | 183 | // Deprecated: Use GetVotesResponse.ProtoReflect.Descriptor instead. 184 | func (*GetVotesResponse) Descriptor() ([]byte, []int) { 185 | return file_goat_v1_goat_proto_rawDescGZIP(), []int{2} 186 | } 187 | 188 | func (x *GetVotesResponse) GetYes() int64 { 189 | if x != nil { 190 | return x.Yes 191 | } 192 | return 0 193 | } 194 | 195 | func (x *GetVotesResponse) GetNo() int64 { 196 | if x != nil { 197 | return x.No 198 | } 199 | return 0 200 | } 201 | 202 | type VoteResponse struct { 203 | state protoimpl.MessageState `protogen:"open.v1"` 204 | Success bool `protobuf:"varint,1,opt,name=Success,proto3" json:"Success,omitempty"` 205 | unknownFields protoimpl.UnknownFields 206 | sizeCache protoimpl.SizeCache 207 | } 208 | 209 | func (x *VoteResponse) Reset() { 210 | *x = VoteResponse{} 211 | mi := &file_goat_v1_goat_proto_msgTypes[3] 212 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 213 | ms.StoreMessageInfo(mi) 214 | } 215 | 216 | func (x *VoteResponse) String() string { 217 | return protoimpl.X.MessageStringOf(x) 218 | } 219 | 220 | func (*VoteResponse) ProtoMessage() {} 221 | 222 | func (x *VoteResponse) ProtoReflect() protoreflect.Message { 223 | mi := &file_goat_v1_goat_proto_msgTypes[3] 224 | if x != nil { 225 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 226 | if ms.LoadMessageInfo() == nil { 227 | ms.StoreMessageInfo(mi) 228 | } 229 | return ms 230 | } 231 | return mi.MessageOf(x) 232 | } 233 | 234 | // Deprecated: Use VoteResponse.ProtoReflect.Descriptor instead. 235 | func (*VoteResponse) Descriptor() ([]byte, []int) { 236 | return file_goat_v1_goat_proto_rawDescGZIP(), []int{3} 237 | } 238 | 239 | func (x *VoteResponse) GetSuccess() bool { 240 | if x != nil { 241 | return x.Success 242 | } 243 | return false 244 | } 245 | 246 | var File_goat_v1_goat_proto protoreflect.FileDescriptor 247 | 248 | var file_goat_v1_goat_proto_rawDesc = string([]byte{ 249 | 0x0a, 0x12, 0x67, 0x6f, 0x61, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x67, 0x6f, 0x61, 0x74, 0x2e, 0x70, 250 | 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x67, 0x6f, 0x61, 0x74, 0x2e, 0x76, 0x31, 0x22, 0x30, 0x0a, 251 | 0x0b, 0x56, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x04, 252 | 0x56, 0x6f, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x0d, 0x2e, 0x67, 0x6f, 0x61, 253 | 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x6f, 0x74, 0x65, 0x52, 0x04, 0x56, 0x6f, 0x74, 0x65, 0x22, 254 | 0x11, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x56, 0x6f, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 255 | 0x73, 0x74, 0x22, 0x34, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x56, 0x6f, 0x74, 0x65, 0x73, 0x52, 0x65, 256 | 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x59, 0x65, 0x73, 0x18, 0x01, 0x20, 257 | 0x01, 0x28, 0x03, 0x52, 0x03, 0x59, 0x65, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x4e, 0x6f, 0x18, 0x02, 258 | 0x20, 0x01, 0x28, 0x03, 0x52, 0x02, 0x4e, 0x6f, 0x22, 0x28, 0x0a, 0x0c, 0x56, 0x6f, 0x74, 0x65, 259 | 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x53, 0x75, 0x63, 0x63, 260 | 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x53, 0x75, 0x63, 0x63, 0x65, 261 | 0x73, 0x73, 0x2a, 0x17, 0x0a, 0x04, 0x56, 0x6f, 0x74, 0x65, 0x12, 0x07, 0x0a, 0x03, 0x59, 0x45, 262 | 0x53, 0x10, 0x00, 0x12, 0x06, 0x0a, 0x02, 0x4e, 0x4f, 0x10, 0x01, 0x32, 0x87, 0x01, 0x0a, 0x0b, 263 | 0x47, 0x6f, 0x61, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x35, 0x0a, 0x04, 0x56, 264 | 0x6f, 0x74, 0x65, 0x12, 0x14, 0x2e, 0x67, 0x6f, 0x61, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x6f, 265 | 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x15, 0x2e, 0x67, 0x6f, 0x61, 0x74, 266 | 0x2e, 0x76, 0x31, 0x2e, 0x56, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 267 | 0x22, 0x00, 0x12, 0x41, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x56, 0x6f, 0x74, 0x65, 0x73, 0x12, 0x18, 268 | 0x2e, 0x67, 0x6f, 0x61, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x6f, 0x74, 0x65, 269 | 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x19, 0x2e, 0x67, 0x6f, 0x61, 0x74, 0x2e, 270 | 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x6f, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 271 | 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x70, 0x0a, 0x0b, 0x63, 0x6f, 0x6d, 0x2e, 0x67, 0x6f, 0x61, 272 | 0x74, 0x2e, 0x76, 0x31, 0x42, 0x09, 0x47, 0x6f, 0x61, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 273 | 0x01, 0x5a, 0x19, 0x67, 0x6f, 0x61, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67, 0x6f, 274 | 0x61, 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x67, 0x6f, 0x61, 0x74, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x47, 275 | 0x58, 0x58, 0xaa, 0x02, 0x07, 0x47, 0x6f, 0x61, 0x74, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x07, 0x47, 276 | 0x6f, 0x61, 0x74, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x13, 0x47, 0x6f, 0x61, 0x74, 0x5c, 0x56, 0x31, 277 | 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x08, 0x47, 278 | 0x6f, 0x61, 0x74, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 279 | }) 280 | 281 | var ( 282 | file_goat_v1_goat_proto_rawDescOnce sync.Once 283 | file_goat_v1_goat_proto_rawDescData []byte 284 | ) 285 | 286 | func file_goat_v1_goat_proto_rawDescGZIP() []byte { 287 | file_goat_v1_goat_proto_rawDescOnce.Do(func() { 288 | file_goat_v1_goat_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_goat_v1_goat_proto_rawDesc), len(file_goat_v1_goat_proto_rawDesc))) 289 | }) 290 | return file_goat_v1_goat_proto_rawDescData 291 | } 292 | 293 | var file_goat_v1_goat_proto_enumTypes = make([]protoimpl.EnumInfo, 1) 294 | var file_goat_v1_goat_proto_msgTypes = make([]protoimpl.MessageInfo, 4) 295 | var file_goat_v1_goat_proto_goTypes = []any{ 296 | (Vote)(0), // 0: goat.v1.Vote 297 | (*VoteRequest)(nil), // 1: goat.v1.VoteRequest 298 | (*GetVotesRequest)(nil), // 2: goat.v1.GetVotesRequest 299 | (*GetVotesResponse)(nil), // 3: goat.v1.GetVotesResponse 300 | (*VoteResponse)(nil), // 4: goat.v1.VoteResponse 301 | } 302 | var file_goat_v1_goat_proto_depIdxs = []int32{ 303 | 0, // 0: goat.v1.VoteRequest.Vote:type_name -> goat.v1.Vote 304 | 1, // 1: goat.v1.GoatService.Vote:input_type -> goat.v1.VoteRequest 305 | 2, // 2: goat.v1.GoatService.GetVotes:input_type -> goat.v1.GetVotesRequest 306 | 4, // 3: goat.v1.GoatService.Vote:output_type -> goat.v1.VoteResponse 307 | 3, // 4: goat.v1.GoatService.GetVotes:output_type -> goat.v1.GetVotesResponse 308 | 3, // [3:5] is the sub-list for method output_type 309 | 1, // [1:3] is the sub-list for method input_type 310 | 1, // [1:1] is the sub-list for extension type_name 311 | 1, // [1:1] is the sub-list for extension extendee 312 | 0, // [0:1] is the sub-list for field type_name 313 | } 314 | 315 | func init() { file_goat_v1_goat_proto_init() } 316 | func file_goat_v1_goat_proto_init() { 317 | if File_goat_v1_goat_proto != nil { 318 | return 319 | } 320 | type x struct{} 321 | out := protoimpl.TypeBuilder{ 322 | File: protoimpl.DescBuilder{ 323 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 324 | RawDescriptor: unsafe.Slice(unsafe.StringData(file_goat_v1_goat_proto_rawDesc), len(file_goat_v1_goat_proto_rawDesc)), 325 | NumEnums: 1, 326 | NumMessages: 4, 327 | NumExtensions: 0, 328 | NumServices: 1, 329 | }, 330 | GoTypes: file_goat_v1_goat_proto_goTypes, 331 | DependencyIndexes: file_goat_v1_goat_proto_depIdxs, 332 | EnumInfos: file_goat_v1_goat_proto_enumTypes, 333 | MessageInfos: file_goat_v1_goat_proto_msgTypes, 334 | }.Build() 335 | File_goat_v1_goat_proto = out.File 336 | file_goat_v1_goat_proto_goTypes = nil 337 | file_goat_v1_goat_proto_depIdxs = nil 338 | } 339 | -------------------------------------------------------------------------------- /apps/server/proto/goat/v1/goatv1connect/goat.connect.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-connect-go. DO NOT EDIT. 2 | // 3 | // Source: goat/v1/goat.proto 4 | 5 | package goatv1connect 6 | 7 | import ( 8 | connect "connectrpc.com/connect" 9 | context "context" 10 | errors "errors" 11 | v1 "goat/proto/goat/v1" 12 | http "net/http" 13 | strings "strings" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file and the connect package are 17 | // compatible. If you get a compiler error that this constant is not defined, this code was 18 | // generated with a version of connect newer than the one compiled into your binary. You can fix the 19 | // problem by either regenerating this code with an older version of connect or updating the connect 20 | // version compiled into your binary. 21 | const _ = connect.IsAtLeastVersion1_13_0 22 | 23 | const ( 24 | // GoatServiceName is the fully-qualified name of the GoatService service. 25 | GoatServiceName = "goat.v1.GoatService" 26 | ) 27 | 28 | // These constants are the fully-qualified names of the RPCs defined in this package. They're 29 | // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. 30 | // 31 | // Note that these are different from the fully-qualified method names used by 32 | // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to 33 | // reflection-formatted method names, remove the leading slash and convert the remaining slash to a 34 | // period. 35 | const ( 36 | // GoatServiceVoteProcedure is the fully-qualified name of the GoatService's Vote RPC. 37 | GoatServiceVoteProcedure = "/goat.v1.GoatService/Vote" 38 | // GoatServiceGetVotesProcedure is the fully-qualified name of the GoatService's GetVotes RPC. 39 | GoatServiceGetVotesProcedure = "/goat.v1.GoatService/GetVotes" 40 | ) 41 | 42 | // GoatServiceClient is a client for the goat.v1.GoatService service. 43 | type GoatServiceClient interface { 44 | Vote(context.Context, *connect.Request[v1.VoteRequest]) (*connect.Response[v1.VoteResponse], error) 45 | GetVotes(context.Context, *connect.Request[v1.GetVotesRequest]) (*connect.Response[v1.GetVotesResponse], error) 46 | } 47 | 48 | // NewGoatServiceClient constructs a client for the goat.v1.GoatService service. By default, it uses 49 | // the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends 50 | // uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or 51 | // connect.WithGRPCWeb() options. 52 | // 53 | // The URL supplied here should be the base URL for the Connect or gRPC server (for example, 54 | // http://api.acme.com or https://acme.com/grpc). 55 | func NewGoatServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) GoatServiceClient { 56 | baseURL = strings.TrimRight(baseURL, "/") 57 | goatServiceMethods := v1.File_goat_v1_goat_proto.Services().ByName("GoatService").Methods() 58 | return &goatServiceClient{ 59 | vote: connect.NewClient[v1.VoteRequest, v1.VoteResponse]( 60 | httpClient, 61 | baseURL+GoatServiceVoteProcedure, 62 | connect.WithSchema(goatServiceMethods.ByName("Vote")), 63 | connect.WithClientOptions(opts...), 64 | ), 65 | getVotes: connect.NewClient[v1.GetVotesRequest, v1.GetVotesResponse]( 66 | httpClient, 67 | baseURL+GoatServiceGetVotesProcedure, 68 | connect.WithSchema(goatServiceMethods.ByName("GetVotes")), 69 | connect.WithClientOptions(opts...), 70 | ), 71 | } 72 | } 73 | 74 | // goatServiceClient implements GoatServiceClient. 75 | type goatServiceClient struct { 76 | vote *connect.Client[v1.VoteRequest, v1.VoteResponse] 77 | getVotes *connect.Client[v1.GetVotesRequest, v1.GetVotesResponse] 78 | } 79 | 80 | // Vote calls goat.v1.GoatService.Vote. 81 | func (c *goatServiceClient) Vote(ctx context.Context, req *connect.Request[v1.VoteRequest]) (*connect.Response[v1.VoteResponse], error) { 82 | return c.vote.CallUnary(ctx, req) 83 | } 84 | 85 | // GetVotes calls goat.v1.GoatService.GetVotes. 86 | func (c *goatServiceClient) GetVotes(ctx context.Context, req *connect.Request[v1.GetVotesRequest]) (*connect.Response[v1.GetVotesResponse], error) { 87 | return c.getVotes.CallUnary(ctx, req) 88 | } 89 | 90 | // GoatServiceHandler is an implementation of the goat.v1.GoatService service. 91 | type GoatServiceHandler interface { 92 | Vote(context.Context, *connect.Request[v1.VoteRequest]) (*connect.Response[v1.VoteResponse], error) 93 | GetVotes(context.Context, *connect.Request[v1.GetVotesRequest]) (*connect.Response[v1.GetVotesResponse], error) 94 | } 95 | 96 | // NewGoatServiceHandler builds an HTTP handler from the service implementation. It returns the path 97 | // on which to mount the handler and the handler itself. 98 | // 99 | // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf 100 | // and JSON codecs. They also support gzip compression. 101 | func NewGoatServiceHandler(svc GoatServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { 102 | goatServiceMethods := v1.File_goat_v1_goat_proto.Services().ByName("GoatService").Methods() 103 | goatServiceVoteHandler := connect.NewUnaryHandler( 104 | GoatServiceVoteProcedure, 105 | svc.Vote, 106 | connect.WithSchema(goatServiceMethods.ByName("Vote")), 107 | connect.WithHandlerOptions(opts...), 108 | ) 109 | goatServiceGetVotesHandler := connect.NewUnaryHandler( 110 | GoatServiceGetVotesProcedure, 111 | svc.GetVotes, 112 | connect.WithSchema(goatServiceMethods.ByName("GetVotes")), 113 | connect.WithHandlerOptions(opts...), 114 | ) 115 | return "/goat.v1.GoatService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 116 | switch r.URL.Path { 117 | case GoatServiceVoteProcedure: 118 | goatServiceVoteHandler.ServeHTTP(w, r) 119 | case GoatServiceGetVotesProcedure: 120 | goatServiceGetVotesHandler.ServeHTTP(w, r) 121 | default: 122 | http.NotFound(w, r) 123 | } 124 | }) 125 | } 126 | 127 | // UnimplementedGoatServiceHandler returns CodeUnimplemented from all methods. 128 | type UnimplementedGoatServiceHandler struct{} 129 | 130 | func (UnimplementedGoatServiceHandler) Vote(context.Context, *connect.Request[v1.VoteRequest]) (*connect.Response[v1.VoteResponse], error) { 131 | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("goat.v1.GoatService.Vote is not implemented")) 132 | } 133 | 134 | func (UnimplementedGoatServiceHandler) GetVotes(context.Context, *connect.Request[v1.GetVotesRequest]) (*connect.Response[v1.GetVotesResponse], error) { 135 | return nil, connect.NewError(connect.CodeUnimplemented, errors.New("goat.v1.GoatService.GetVotes is not implemented")) 136 | } 137 | -------------------------------------------------------------------------------- /apps/server/scripts/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | 5 | # Restore the database if it does not already exist. 6 | if [ -f /data/db ]; then 7 | echo "Database already exists, skipping restore" 8 | else 9 | echo "No database found, restoring from replica if exists" 10 | litestream restore -if-replica-exists /data/db 11 | fi 12 | 13 | # Run litestream with your app as the subprocess. 14 | exec litestream replicate -exec "/opt/bin/goat" 15 | -------------------------------------------------------------------------------- /apps/web/.env: -------------------------------------------------------------------------------- 1 | VITE_API_SERVER_URL=http://localhost:8080 2 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | -------------------------------------------------------------------------------- /apps/web/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.watcherExclude": { 3 | "**/routeTree.gen.ts": true 4 | }, 5 | "search.exclude": { 6 | "**/routeTree.gen.ts": true 7 | }, 8 | "files.readonlyInclude": { 9 | "**/routeTree.gen.ts": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest AS base 2 | ENV PNPM_HOME="/pnpm" 3 | ENV PATH="$PNPM_HOME:$PATH" 4 | RUN corepack enable 5 | 6 | 7 | 8 | FROM base AS install 9 | WORKDIR /app/ 10 | ENV PNPM_HOME="/pnpm" 11 | ENV PATH="$PNPM_HOME:$PATH" 12 | RUN corepack enable 13 | 14 | RUN \ 15 | --mount=type=bind,target=package.json,source=package.json \ 16 | --mount=type=bind,target=pnpm-lock.yaml,source=pnpm-lock.yaml \ 17 | --mount=type=bind,target=pnpm-workspace.yaml,source=pnpm-workspace.yaml \ 18 | --mount=type=bind,target=apps/web/package.json,source=apps/web/package.json \ 19 | --mount=type=bind,target=packages/ui/package.json,source=packages/ui/package.json \ 20 | pnpm install --frozen-lockfile --prod 21 | 22 | FROM base AS build 23 | WORKDIR /app/ 24 | COPY \ 25 | --link \ 26 | "." "/app/" 27 | 28 | COPY \ 29 | --from=install \ 30 | --link \ 31 | "/app/node_modules" "/app/node_modules" 32 | 33 | RUN pnpm build 34 | 35 | 36 | FROM nginx:stable-alpine 37 | WORKDIR /app/ 38 | 39 | # Taking advantages from docker multi-staging, we copy our newly generated build from /app to the nginx html folder -entrypoint of the webserver- 40 | COPY --from=build /app/apps/web/dist /usr/share/nginx/html 41 | # We copy the nginx conf file from our machine to our image 42 | COPY /apps/web/nginx/nginx.conf /etc/nginx/conf.d/default.conf 43 | # We expose the port 80 of the future containers 44 | EXPOSE 80 45 | # And finally we can run the nginx command to start the server 46 | CMD ["nginx", "-g", "daemon off;"] 47 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | Welcome to your new TanStack app! 2 | 3 | # Getting Started 4 | 5 | To run this application: 6 | 7 | ```bash 8 | npm install 9 | npm run start 10 | ``` 11 | 12 | # Building For Production 13 | 14 | To build this application for production: 15 | 16 | ```bash 17 | npm run build 18 | ``` 19 | 20 | ## Testing 21 | 22 | This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with: 23 | 24 | ```bash 25 | npm run test 26 | ``` 27 | 28 | ## Styling 29 | 30 | This project uses [Tailwind CSS](https://tailwindcss.com/) for styling. 31 | 32 | 33 | 34 | 35 | 36 | ## Routing 37 | This project uses [TanStack Router](https://tanstack.com/router). The initial setup is a file based router. Which means that the routes are managed as files in `src/routes`. 38 | 39 | ### Adding A Route 40 | 41 | To add a new route to your application just add another a new file in the `./src/routes` directory. 42 | 43 | TanStack will automatically generate the content of the route file for you. 44 | 45 | Now that you have two routes you can use a `Link` component to navigate between them. 46 | 47 | ### Adding Links 48 | 49 | To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`. 50 | 51 | ```tsx 52 | import { Link } from "@tanstack/react-router"; 53 | ``` 54 | 55 | Then anywhere in your JSX you can use it like so: 56 | 57 | ```tsx 58 | About 59 | ``` 60 | 61 | This will create a link that will navigate to the `/about` route. 62 | 63 | More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent). 64 | 65 | ### Using A Layout 66 | 67 | In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you use the `` component. 68 | 69 | Here is an example layout that includes a header: 70 | 71 | ```tsx 72 | import { Outlet, createRootRoute } from '@tanstack/react-router' 73 | import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' 74 | 75 | import { Link } from "@tanstack/react-router"; 76 | 77 | export const Route = createRootRoute({ 78 | component: () => ( 79 | <> 80 |
81 | 85 |
86 | 87 | 88 | 89 | ), 90 | }) 91 | ``` 92 | 93 | The `` component is not required so you can remove it if you don't want it in your layout. 94 | 95 | More information on layouts can be found in the [Layouts documentation](hthttps://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts). 96 | 97 | 98 | ## Data Fetching 99 | 100 | There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered. 101 | 102 | For example: 103 | 104 | ```tsx 105 | const peopleRoute = createRoute({ 106 | getParentRoute: () => rootRoute, 107 | path: "/people", 108 | loader: async () => { 109 | const response = await fetch("https://swapi.dev/api/people"); 110 | return response.json() as Promise<{ 111 | results: { 112 | name: string; 113 | }[]; 114 | }>; 115 | }, 116 | component: () => { 117 | const data = peopleRoute.useLoaderData(); 118 | return ( 119 |
    120 | {data.results.map((person) => ( 121 |
  • {person.name}
  • 122 | ))} 123 |
124 | ); 125 | }, 126 | }); 127 | ``` 128 | 129 | Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters). 130 | 131 | ### React-Query 132 | 133 | React-Query is an excellent addition or alternative to route loading and integrating it into you application is a breeze. 134 | 135 | First add your dependencies: 136 | 137 | ```bash 138 | npm install @tanstack/react-query @tanstack/react-query-devtools 139 | ``` 140 | 141 | Next we'll need to creata query client and provider. We recommend putting those in `main.tsx`. 142 | 143 | ```tsx 144 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 145 | 146 | // ... 147 | 148 | const queryClient = new QueryClient(); 149 | 150 | // ... 151 | 152 | if (!rootElement.innerHTML) { 153 | const root = ReactDOM.createRoot(rootElement); 154 | 155 | root.render( 156 | 157 | 158 | 159 | ); 160 | } 161 | ``` 162 | 163 | You can also add TanStack Query Devtools to the root route (optional). 164 | 165 | ```tsx 166 | import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; 167 | 168 | const rootRoute = createRootRoute({ 169 | component: () => ( 170 | <> 171 | 172 | 173 | 174 | 175 | ), 176 | }); 177 | ``` 178 | 179 | Now you can use `useQuery` to fetch your data. 180 | 181 | ```tsx 182 | import { useQuery } from "@tanstack/react-query"; 183 | 184 | import "./App.css"; 185 | 186 | function App() { 187 | const { data } = useQuery({ 188 | queryKey: ["people"], 189 | queryFn: () => 190 | fetch("https://swapi.dev/api/people") 191 | .then((res) => res.json()) 192 | .then((data) => data.results as { name: string }[]), 193 | initialData: [], 194 | }); 195 | 196 | return ( 197 |
198 |
    199 | {data.map((person) => ( 200 |
  • {person.name}
  • 201 | ))} 202 |
203 |
204 | ); 205 | } 206 | 207 | export default App; 208 | ``` 209 | 210 | You can find out everything you need to know on how to use React-Query in the [React-Query documentation](https://tanstack.com/query/latest/docs/framework/react/overview). 211 | 212 | ## State Management 213 | 214 | Another common requirement for React applications is state management. There are many options for state management in React. TanStack Store provides a great starting point for your project. 215 | 216 | First you need to add TanStack Store as a dependency: 217 | 218 | ```bash 219 | npm install @tanstack/store 220 | ``` 221 | 222 | Now let's create a simple counter in the `src/App.tsx` file as a demonstration. 223 | 224 | ```tsx 225 | import { useStore } from "@tanstack/react-store"; 226 | import { Store } from "@tanstack/store"; 227 | import "./App.css"; 228 | 229 | const countStore = new Store(0); 230 | 231 | function App() { 232 | const count = useStore(countStore); 233 | return ( 234 |
235 | 238 |
239 | ); 240 | } 241 | 242 | export default App; 243 | ``` 244 | 245 | One of the many nice features of TanStack Store is the ability to derive state from other state. That derived state will update when the base state updates. 246 | 247 | Let's check this out by doubling the count using derived state. 248 | 249 | ```tsx 250 | import { useStore } from "@tanstack/react-store"; 251 | import { Store, Derived } from "@tanstack/store"; 252 | import "./App.css"; 253 | 254 | const countStore = new Store(0); 255 | 256 | const doubledStore = new Derived({ 257 | fn: () => countStore.state * 2, 258 | deps: [countStore], 259 | }); 260 | doubledStore.mount(); 261 | 262 | function App() { 263 | const count = useStore(countStore); 264 | const doubledCount = useStore(doubledStore); 265 | 266 | return ( 267 |
268 | 271 |
Doubled - {doubledCount}
272 |
273 | ); 274 | } 275 | 276 | export default App; 277 | ``` 278 | 279 | We use the `Derived` class to create a new store that is derived from another store. The `Derived` class has a `mount` method that will start the derived store updating. 280 | 281 | Once we've created the derived store we can use it in the `App` component just like we would any other store using the `useStore` hook. 282 | 283 | You can find out everything you need to know on how to use TanStack Store in the [TanStack Store documentation](https://tanstack.com/store/latest). 284 | 285 | # Demo files 286 | 287 | Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed. 288 | 289 | # Learn More 290 | 291 | You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com). 292 | -------------------------------------------------------------------------------- /apps/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Create TanStack App - web 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /apps/web/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | 3 | listen 80; 4 | 5 | location / { 6 | root /usr/share/nginx/html; 7 | index index.html index.htm; 8 | try_files $uri $uri/ /index.html; 9 | } 10 | 11 | error_page 500 502 503 504 /50x.html; 12 | 13 | location = /50x.html { 14 | root /usr/share/nginx/html; 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@goat/web", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build && tsc", 8 | "serve": "vite preview", 9 | "test": "vitest run" 10 | }, 11 | "dependencies": { 12 | "@bufbuild/protobuf": "2.2.4", 13 | "@connectrpc/connect-query": "2.0.1", 14 | "@connectrpc/connect-web": "2.0.2", 15 | "@goat/ui": "workspace:*", 16 | "@tailwindcss/vite": "4.0.6", 17 | "@tanstack/react-query": "5.68.0", 18 | "@tanstack/react-router": "1.114.23", 19 | "@tanstack/react-router-devtools": "1.114.23", 20 | "@tanstack/router-plugin": "1.114.23", 21 | "react": "19.0.0", 22 | "react-dom": "19.0.0", 23 | "recharts": "2.15.1", 24 | "tailwindcss": "4.0.6", 25 | "tailwindcss-animate": "1.0.7" 26 | }, 27 | "devDependencies": { 28 | "@testing-library/dom": "10.4.0", 29 | "@testing-library/react": "16.2.0", 30 | "@types/react": "19.0.8", 31 | "@types/react-dom": "19.0.3", 32 | "@vitejs/plugin-react": "4.3.4", 33 | "jsdom": "26.0.0", 34 | "typescript": "5.7.2", 35 | "vite": "6.1.0", 36 | "vitest": "3.0.5", 37 | "web-vitals": "4.2.4" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /apps/web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstatusHQ/goat-stack/734e04059f1f6f76a5fea392c5f8baf856fcd35a/apps/web/public/favicon.ico -------------------------------------------------------------------------------- /apps/web/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstatusHQ/goat-stack/734e04059f1f6f76a5fea392c5f8baf856fcd35a/apps/web/public/logo192.png -------------------------------------------------------------------------------- /apps/web/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstatusHQ/goat-stack/734e04059f1f6f76a5fea392c5f8baf856fcd35a/apps/web/public/logo512.png -------------------------------------------------------------------------------- /apps/web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "TanStack App", 3 | "name": "Create TanStack App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /apps/web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /apps/web/src/gen/proto/goat/v1/goat-GoatService_connectquery.ts: -------------------------------------------------------------------------------- 1 | // @generated by protoc-gen-connect-query v2.0.1 with parameter "target=ts" 2 | // @generated from file goat/v1/goat.proto (package goat.v1, syntax proto3) 3 | /* eslint-disable */ 4 | 5 | import { GoatService } from "./goat_pb"; 6 | 7 | /** 8 | * @generated from rpc goat.v1.GoatService.Vote 9 | */ 10 | export const vote = GoatService.method.vote; 11 | 12 | /** 13 | * @generated from rpc goat.v1.GoatService.GetVotes 14 | */ 15 | export const getVotes = GoatService.method.getVotes; 16 | -------------------------------------------------------------------------------- /apps/web/src/gen/proto/goat/v1/goat_pb.ts: -------------------------------------------------------------------------------- 1 | // @generated by protoc-gen-es v2.2.4 with parameter "target=ts" 2 | // @generated from file goat/v1/goat.proto (package goat.v1, syntax proto3) 3 | /* eslint-disable */ 4 | 5 | import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv1"; 6 | import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv1"; 7 | import type { Message } from "@bufbuild/protobuf"; 8 | 9 | /** 10 | * Describes the file goat/v1/goat.proto. 11 | */ 12 | export const file_goat_v1_goat: GenFile = /*@__PURE__*/ 13 | fileDesc("ChJnb2F0L3YxL2dvYXQucHJvdG8SB2dvYXQudjEiKgoLVm90ZVJlcXVlc3QSGwoEVm90ZRgBIAEoDjINLmdvYXQudjEuVm90ZSIRCg9HZXRWb3Rlc1JlcXVlc3QiKwoQR2V0Vm90ZXNSZXNwb25zZRILCgNZZXMYASABKAMSCgoCTm8YAiABKAMiHwoMVm90ZVJlc3BvbnNlEg8KB1N1Y2Nlc3MYASABKAgqFwoEVm90ZRIHCgNZRVMQABIGCgJOTxABMocBCgtHb2F0U2VydmljZRI1CgRWb3RlEhQuZ29hdC52MS5Wb3RlUmVxdWVzdBoVLmdvYXQudjEuVm90ZVJlc3BvbnNlIgASQQoIR2V0Vm90ZXMSGC5nb2F0LnYxLkdldFZvdGVzUmVxdWVzdBoZLmdvYXQudjEuR2V0Vm90ZXNSZXNwb25zZSIAQnAKC2NvbS5nb2F0LnYxQglHb2F0UHJvdG9QAVoZZ29hdC9wcm90by9nb2F0L3YxO2dvYXR2MaICA0dYWKoCB0dvYXQuVjHKAgdHb2F0XFYx4gITR29hdFxWMVxHUEJNZXRhZGF0YeoCCEdvYXQ6OlYxYgZwcm90bzM"); 14 | 15 | /** 16 | * @generated from message goat.v1.VoteRequest 17 | */ 18 | export type VoteRequest = Message<"goat.v1.VoteRequest"> & { 19 | /** 20 | * @generated from field: goat.v1.Vote Vote = 1; 21 | */ 22 | Vote: Vote; 23 | }; 24 | 25 | /** 26 | * Describes the message goat.v1.VoteRequest. 27 | * Use `create(VoteRequestSchema)` to create a new message. 28 | */ 29 | export const VoteRequestSchema: GenMessage = /*@__PURE__*/ 30 | messageDesc(file_goat_v1_goat, 0); 31 | 32 | /** 33 | * @generated from message goat.v1.GetVotesRequest 34 | */ 35 | export type GetVotesRequest = Message<"goat.v1.GetVotesRequest"> & { 36 | }; 37 | 38 | /** 39 | * Describes the message goat.v1.GetVotesRequest. 40 | * Use `create(GetVotesRequestSchema)` to create a new message. 41 | */ 42 | export const GetVotesRequestSchema: GenMessage = /*@__PURE__*/ 43 | messageDesc(file_goat_v1_goat, 1); 44 | 45 | /** 46 | * @generated from message goat.v1.GetVotesResponse 47 | */ 48 | export type GetVotesResponse = Message<"goat.v1.GetVotesResponse"> & { 49 | /** 50 | * @generated from field: int64 Yes = 1; 51 | */ 52 | Yes: bigint; 53 | 54 | /** 55 | * @generated from field: int64 No = 2; 56 | */ 57 | No: bigint; 58 | }; 59 | 60 | /** 61 | * Describes the message goat.v1.GetVotesResponse. 62 | * Use `create(GetVotesResponseSchema)` to create a new message. 63 | */ 64 | export const GetVotesResponseSchema: GenMessage = /*@__PURE__*/ 65 | messageDesc(file_goat_v1_goat, 2); 66 | 67 | /** 68 | * @generated from message goat.v1.VoteResponse 69 | */ 70 | export type VoteResponse = Message<"goat.v1.VoteResponse"> & { 71 | /** 72 | * @generated from field: bool Success = 1; 73 | */ 74 | Success: boolean; 75 | }; 76 | 77 | /** 78 | * Describes the message goat.v1.VoteResponse. 79 | * Use `create(VoteResponseSchema)` to create a new message. 80 | */ 81 | export const VoteResponseSchema: GenMessage = /*@__PURE__*/ 82 | messageDesc(file_goat_v1_goat, 3); 83 | 84 | /** 85 | * @generated from enum goat.v1.Vote 86 | */ 87 | export enum Vote { 88 | /** 89 | * @generated from enum value: YES = 0; 90 | */ 91 | YES = 0, 92 | 93 | /** 94 | * @generated from enum value: NO = 1; 95 | */ 96 | NO = 1, 97 | } 98 | 99 | /** 100 | * Describes the enum goat.v1.Vote. 101 | */ 102 | export const VoteSchema: GenEnum = /*@__PURE__*/ 103 | enumDesc(file_goat_v1_goat, 0); 104 | 105 | /** 106 | * @generated from service goat.v1.GoatService 107 | */ 108 | export const GoatService: GenService<{ 109 | /** 110 | * @generated from rpc goat.v1.GoatService.Vote 111 | */ 112 | vote: { 113 | methodKind: "unary"; 114 | input: typeof VoteRequestSchema; 115 | output: typeof VoteResponseSchema; 116 | }, 117 | /** 118 | * @generated from rpc goat.v1.GoatService.GetVotes 119 | */ 120 | getVotes: { 121 | methodKind: "unary"; 122 | input: typeof GetVotesRequestSchema; 123 | output: typeof GetVotesResponseSchema; 124 | }, 125 | }> = /*@__PURE__*/ 126 | serviceDesc(file_goat_v1_goat, 0); 127 | 128 | -------------------------------------------------------------------------------- /apps/web/src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /apps/web/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { StrictMode } from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import { RouterProvider, createRouter } from "@tanstack/react-router"; 4 | 5 | // Import the generated route tree 6 | import { routeTree } from "./routeTree.gen"; 7 | import "./styles.css"; 8 | import reportWebVitals from "./reportWebVitals.ts"; 9 | import { createConnectTransport } from "@connectrpc/connect-web"; 10 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 11 | import { TransportProvider } from "@connectrpc/connect-query"; 12 | 13 | const finalTransport = createConnectTransport({ 14 | baseUrl: import.meta.env.VITE_API_SERVER_URL, 15 | }); 16 | 17 | const queryClient = new QueryClient(); 18 | 19 | // Create a new router instance 20 | const router = createRouter({ 21 | routeTree, 22 | context: {}, 23 | defaultPreload: "intent", 24 | scrollRestoration: true, 25 | defaultStructuralSharing: true, 26 | }); 27 | 28 | // Register the router instance for type safety 29 | declare module "@tanstack/react-router" { 30 | interface Register { 31 | router: typeof router; 32 | } 33 | } 34 | 35 | // Render the app 36 | const rootElement = document.getElementById("app"); 37 | if (rootElement && !rootElement.innerHTML) { 38 | const root = ReactDOM.createRoot(rootElement); 39 | root.render( 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ); 48 | } 49 | 50 | // If you want to start measuring performance in your app, pass a function 51 | // to log results (for example: reportWebVitals(console.log)) 52 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 53 | reportWebVitals(); 54 | -------------------------------------------------------------------------------- /apps/web/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry?: () => void) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ onCLS, onINP, onFCP, onLCP, onTTFB }) => { 4 | onCLS(onPerfEntry) 5 | onINP(onPerfEntry) 6 | onFCP(onPerfEntry) 7 | onLCP(onPerfEntry) 8 | onTTFB(onPerfEntry) 9 | }) 10 | } 11 | } 12 | 13 | export default reportWebVitals 14 | -------------------------------------------------------------------------------- /apps/web/src/routeTree.gen.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | // @ts-nocheck 4 | 5 | // noinspection JSUnusedGlobalSymbols 6 | 7 | // This file was automatically generated by TanStack Router. 8 | // You should NOT make any changes in this file as it will be overwritten. 9 | // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. 10 | 11 | // Import Routes 12 | 13 | import { Route as rootRoute } from './routes/__root' 14 | import { Route as ResultsImport } from './routes/results' 15 | import { Route as IndexImport } from './routes/index' 16 | 17 | // Create/Update Routes 18 | 19 | const ResultsRoute = ResultsImport.update({ 20 | id: '/results', 21 | path: '/results', 22 | getParentRoute: () => rootRoute, 23 | } as any) 24 | 25 | const IndexRoute = IndexImport.update({ 26 | id: '/', 27 | path: '/', 28 | getParentRoute: () => rootRoute, 29 | } as any) 30 | 31 | // Populate the FileRoutesByPath interface 32 | 33 | declare module '@tanstack/react-router' { 34 | interface FileRoutesByPath { 35 | '/': { 36 | id: '/' 37 | path: '/' 38 | fullPath: '/' 39 | preLoaderRoute: typeof IndexImport 40 | parentRoute: typeof rootRoute 41 | } 42 | '/results': { 43 | id: '/results' 44 | path: '/results' 45 | fullPath: '/results' 46 | preLoaderRoute: typeof ResultsImport 47 | parentRoute: typeof rootRoute 48 | } 49 | } 50 | } 51 | 52 | // Create and export the route tree 53 | 54 | export interface FileRoutesByFullPath { 55 | '/': typeof IndexRoute 56 | '/results': typeof ResultsRoute 57 | } 58 | 59 | export interface FileRoutesByTo { 60 | '/': typeof IndexRoute 61 | '/results': typeof ResultsRoute 62 | } 63 | 64 | export interface FileRoutesById { 65 | __root__: typeof rootRoute 66 | '/': typeof IndexRoute 67 | '/results': typeof ResultsRoute 68 | } 69 | 70 | export interface FileRouteTypes { 71 | fileRoutesByFullPath: FileRoutesByFullPath 72 | fullPaths: '/' | '/results' 73 | fileRoutesByTo: FileRoutesByTo 74 | to: '/' | '/results' 75 | id: '__root__' | '/' | '/results' 76 | fileRoutesById: FileRoutesById 77 | } 78 | 79 | export interface RootRouteChildren { 80 | IndexRoute: typeof IndexRoute 81 | ResultsRoute: typeof ResultsRoute 82 | } 83 | 84 | const rootRouteChildren: RootRouteChildren = { 85 | IndexRoute: IndexRoute, 86 | ResultsRoute: ResultsRoute, 87 | } 88 | 89 | export const routeTree = rootRoute 90 | ._addFileChildren(rootRouteChildren) 91 | ._addFileTypes() 92 | 93 | /* ROUTE_MANIFEST_START 94 | { 95 | "routes": { 96 | "__root__": { 97 | "filePath": "__root.tsx", 98 | "children": [ 99 | "/", 100 | "/results" 101 | ] 102 | }, 103 | "/": { 104 | "filePath": "index.tsx" 105 | }, 106 | "/results": { 107 | "filePath": "results.tsx" 108 | } 109 | } 110 | } 111 | ROUTE_MANIFEST_END */ 112 | -------------------------------------------------------------------------------- /apps/web/src/routes/__root.tsx: -------------------------------------------------------------------------------- 1 | import { Outlet, createRootRoute } from '@tanstack/react-router' 2 | import { TanStackRouterDevtools } from '@tanstack/react-router-devtools' 3 | 4 | export const Route = createRootRoute({ 5 | component: () => ( 6 | <> 7 | 8 | 9 | 10 | ), 11 | }) 12 | -------------------------------------------------------------------------------- /apps/web/src/routes/index.tsx: -------------------------------------------------------------------------------- 1 | import { useMutation } from "@connectrpc/connect-query"; 2 | import { createFileRoute, Link, useRouter } from "@tanstack/react-router"; 3 | import { vote } from "../gen/proto/goat/v1/goat-GoatService_connectquery"; 4 | import { Vote } from "../gen/proto/goat/v1/goat_pb"; 5 | import { Button } from "@goat/ui/components/button"; 6 | 7 | export const Route = createFileRoute("/")({ 8 | component: App, 9 | }); 10 | 11 | function App() { 12 | const v = useMutation(vote); 13 | const { navigate } = useRouter(); 14 | return ( 15 |
16 |
17 |

Is this the 🐐 stack?

18 | 19 |
20 | 32 | 44 |

45 | {" "} 46 | 49 |

50 |
51 | 52 |
53 | Because we just ship it at{" "} 54 | 58 | OpenStatus 59 | 60 | . Here's the goat-stack{" "} 61 |
62 | 63 | 64 | 65 | 73 |
74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /apps/web/src/routes/results.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery } from "@connectrpc/connect-query"; 2 | import { createFileRoute } from "@tanstack/react-router"; 3 | import { getVotes } from "../gen/proto/goat/v1/goat-GoatService_connectquery"; 4 | import { 5 | ChartContainer, 6 | ChartTooltip, 7 | ChartTooltipContent, 8 | type ChartConfig, 9 | } from "@goat/ui/components/charts"; 10 | import { Pie, PieChart } from "recharts"; 11 | 12 | export const Route = createFileRoute("/results")({ 13 | component: RouteComponent, 14 | }); 15 | 16 | const chartConfig = { 17 | yes: { 18 | label: "Yes", 19 | color: "var(--chart-1)", 20 | }, 21 | no: { 22 | label: "No", 23 | color: "var(--chart-2)", 24 | }, 25 | } satisfies ChartConfig; 26 | 27 | function RouteComponent() { 28 | const data = useQuery(getVotes, {}); 29 | 30 | if (data.isLoading) { 31 | return null; 32 | } 33 | 34 | const chartData = [ 35 | { 36 | name: "Yes", 37 | value: Number(data.data?.Yes), 38 | fill: "var(--color-yes)", 39 | }, 40 | { 41 | name: "No", 42 | value: Number(data.data?.No), 43 | fill: "var(--color-no)", 44 | }, 45 | ]; 46 | return ( 47 |
48 |

Is it the goat stack??

49 | 50 | 51 | 52 | 53 | } /> 54 | 55 | 56 |

Star us on GitHub

57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /apps/web/src/styles.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | @plugin 'tailwindcss-animate'; 4 | 5 | @custom-variant dark (&:is(.dark *)); 6 | 7 | :root { 8 | --background: oklch(1 0 0); 9 | --foreground: oklch(0.145 0 0); 10 | --card: oklch(1 0 0); 11 | --card-foreground: oklch(0.145 0 0); 12 | --popover: oklch(1 0 0); 13 | --popover-foreground: oklch(0.145 0 0); 14 | --primary: oklch(0.205 0 0); 15 | --primary-foreground: oklch(0.985 0 0); 16 | --secondary: oklch(0.97 0 0); 17 | --secondary-foreground: oklch(0.205 0 0); 18 | --muted: oklch(0.97 0 0); 19 | --muted-foreground: oklch(0.556 0 0); 20 | --accent: oklch(0.97 0 0); 21 | --accent-foreground: oklch(0.205 0 0); 22 | --destructive: oklch(0.577 0.245 27.325); 23 | --destructive-foreground: oklch(0.577 0.245 27.325); 24 | --border: oklch(0.922 0 0); 25 | --input: oklch(0.922 0 0); 26 | --ring: oklch(0.708 0 0); 27 | --chart-1: oklch(54.61% 0.2152 262.88); 28 | --chart-2: oklch(58.77% 0.2229 17.68); 29 | --chart-3: oklch(0.398 0.07 227.392); 30 | --chart-4: oklch(0.828 0.189 84.429); 31 | --chart-5: oklch(0.769 0.188 70.08); 32 | --radius: 0.625rem; 33 | --sidebar: oklch(0.985 0 0); 34 | --sidebar-foreground: oklch(0.145 0 0); 35 | --sidebar-primary: oklch(0.205 0 0); 36 | --sidebar-primary-foreground: oklch(0.985 0 0); 37 | --sidebar-accent: oklch(0.97 0 0); 38 | --sidebar-accent-foreground: oklch(0.205 0 0); 39 | --sidebar-border: oklch(0.922 0 0); 40 | --sidebar-ring: oklch(0.708 0 0); 41 | 42 | } 43 | 44 | .dark { 45 | --background: oklch(0.145 0 0); 46 | --foreground: oklch(0.985 0 0); 47 | --card: oklch(0.145 0 0); 48 | --card-foreground: oklch(0.985 0 0); 49 | --popover: oklch(0.145 0 0); 50 | --popover-foreground: oklch(0.985 0 0); 51 | --primary: oklch(0.985 0 0); 52 | --primary-foreground: oklch(0.205 0 0); 53 | --secondary: oklch(0.269 0 0); 54 | --secondary-foreground: oklch(0.985 0 0); 55 | --muted: oklch(0.269 0 0); 56 | --muted-foreground: oklch(0.708 0 0); 57 | --accent: oklch(0.269 0 0); 58 | --accent-foreground: oklch(0.985 0 0); 59 | --destructive: oklch(0.396 0.141 25.723); 60 | --destructive-foreground: oklch(0.637 0.237 25.331); 61 | --border: oklch(0.269 0 0); 62 | --input: oklch(0.269 0 0); 63 | --ring: oklch(0.439 0 0); 64 | --chart-1: oklch(0.488 0.243 264.376); 65 | --chart-2: oklch(0.696 0.17 162.48); 66 | --chart-3: oklch(0.769 0.188 70.08); 67 | --chart-4: oklch(0.627 0.265 303.9); 68 | --chart-5: oklch(0.645 0.246 16.439); 69 | --sidebar: oklch(0.205 0 0); 70 | --sidebar-foreground: oklch(0.985 0 0); 71 | --sidebar-primary: oklch(0.488 0.243 264.376); 72 | --sidebar-primary-foreground: oklch(0.985 0 0); 73 | --sidebar-accent: oklch(0.269 0 0); 74 | --sidebar-accent-foreground: oklch(0.985 0 0); 75 | --sidebar-border: oklch(0.269 0 0); 76 | --sidebar-ring: oklch(0.439 0 0); 77 | 78 | 79 | } 80 | 81 | @theme inline { 82 | --color-background: var(--background); 83 | --color-foreground: var(--foreground); 84 | --color-card: var(--card); 85 | --color-card-foreground: var(--card-foreground); 86 | --color-popover: var(--popover); 87 | --color-popover-foreground: var(--popover-foreground); 88 | --color-primary: var(--primary); 89 | --color-primary-foreground: var(--primary-foreground); 90 | --color-secondary: var(--secondary); 91 | --color-secondary-foreground: var(--secondary-foreground); 92 | --color-muted: var(--muted); 93 | --color-muted-foreground: var(--muted-foreground); 94 | --color-accent: var(--accent); 95 | --color-accent-foreground: var(--accent-foreground); 96 | --color-destructive: var(--destructive); 97 | --color-destructive-foreground: var(--destructive-foreground); 98 | --color-border: var(--border); 99 | --color-input: var(--input); 100 | --color-ring: var(--ring); 101 | --color-chart-1: var(--chart-1); 102 | --color-chart-2: var(--chart-2); 103 | --color-chart-3: var(--chart-3); 104 | --color-chart-4: var(--chart-4); 105 | --color-chart-5: var(--chart-5); 106 | --radius-sm: calc(var(--radius) - 4px); 107 | --radius-md: calc(var(--radius) - 2px); 108 | --radius-lg: var(--radius); 109 | --radius-xl: calc(var(--radius) + 4px); 110 | --color-sidebar: var(--sidebar); 111 | --color-sidebar-foreground: var(--sidebar-foreground); 112 | --color-sidebar-primary: var(--sidebar-primary); 113 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 114 | --color-sidebar-accent: var(--sidebar-accent); 115 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 116 | --color-sidebar-border: var(--sidebar-border); 117 | --color-sidebar-ring: var(--sidebar-ring); 118 | } 119 | 120 | @layer base { 121 | * { 122 | @apply border-border outline-ring/50; 123 | } 124 | body { 125 | @apply bg-background text-foreground; 126 | } 127 | :root { 128 | --chart-1: 12 76% 61%; 129 | --chart-2: 173 58% 39%; 130 | --chart-3: 197 37% 24%; 131 | --chart-4: 43 74% 66%; 132 | --chart-5: 27 87% 67%; 133 | } 134 | 135 | .dark { 136 | --chart-1: 220 70% 50%; 137 | --chart-2: 160 60% 45%; 138 | --chart-3: 30 80% 55%; 139 | --chart-4: 280 65% 60%; 140 | --chart-5: 340 75% 55%; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*.ts", "**/*.tsx"], 3 | "compilerOptions": { 4 | "target": "ES2022", 5 | "jsx": "react-jsx", 6 | "module": "ESNext", 7 | "lib": ["ES2022", "DOM", "DOM.Iterable"], 8 | "types": ["vite/client"], 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "skipLibCheck": true, 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noUncheckedSideEffectImports": true, 23 | "paths": { 24 | "@goat/ui/*": ["../../packages/ui/src/*"] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apps/web/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import viteReact from "@vitejs/plugin-react"; 3 | import tailwindcss from "@tailwindcss/vite"; 4 | 5 | import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [TanStackRouterVite({ autoCodeSplitting: true }), viteReact(), tailwindcss()], 10 | test: { 11 | globals: true, 12 | environment: "jsdom", 13 | }, 14 | 15 | }); 16 | -------------------------------------------------------------------------------- /goat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openstatusHQ/goat-stack/734e04059f1f6f76a5fea392c5f8baf856fcd35a/goat.png -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | init: 2 | pnpm install 3 | just packages/proto/init 4 | @echo "You are good to go!" 5 | 6 | buf: 7 | rm -rf apps/server/proto 8 | rm -rf apps/web/src/gen/proto 9 | just packages/proto/buf 10 | 11 | 12 | dev: 13 | pnpm dev 14 | 15 | build: 16 | just apps/server/build 17 | pnpm build 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "turbo build", 5 | "dev": "turbo run dev", 6 | "lint": "turbo run lint", 7 | "check-types": "turbo check-types", 8 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 9 | }, 10 | "devDependencies": { 11 | "prettier": "3.5.0", 12 | "prettier-plugin-tailwindcss": "0.6.11", 13 | "turbo": "2.4.1" 14 | }, 15 | "packageManager": "pnpm@10.6.0", 16 | "engines": { 17 | "node": ">=22" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/proto/buf.gen.yaml: -------------------------------------------------------------------------------- 1 | # buf.gen.yaml defines a local generation template. 2 | # For details, see https://buf.build/docs/configuration/v2/buf-gen-yaml 3 | version: v2 4 | managed: 5 | enabled: true 6 | override: 7 | - file_option: go_package_prefix 8 | value: goat/proto 9 | plugins: 10 | - local: protoc-gen-go 11 | out: ../../apps/server/proto 12 | opt: paths=source_relative 13 | - local: protoc-gen-connect-go 14 | out: ../../apps/server/proto 15 | opt: paths=source_relative 16 | - local: protoc-gen-es 17 | out: ../../apps/web/src/gen/proto 18 | opt: target=ts 19 | include_imports: true 20 | - local: protoc-gen-connect-query 21 | out: ../../apps/web/src/gen/proto 22 | opt: target=ts 23 | include_imports: true 24 | -------------------------------------------------------------------------------- /packages/proto/goat/v1/goat.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package goat.v1; 4 | 5 | 6 | service GoatService { 7 | rpc Vote(VoteRequest) returns (VoteResponse) {} 8 | rpc GetVotes(GetVotesRequest) returns (GetVotesResponse) {} 9 | } 10 | 11 | enum Vote { 12 | YES = 0; 13 | NO = 1; 14 | } 15 | 16 | message VoteRequest { 17 | Vote Vote = 1; 18 | } 19 | 20 | message GetVotesRequest {} 21 | 22 | message GetVotesResponse { 23 | int64 Yes = 1; 24 | int64 No = 2; 25 | } 26 | message VoteResponse { 27 | bool Success = 1; 28 | } 29 | -------------------------------------------------------------------------------- /packages/proto/justfile: -------------------------------------------------------------------------------- 1 | set fallback := true 2 | 3 | init: 4 | go install github.com/bufbuild/buf/cmd/buf@latest 5 | go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest 6 | go install google.golang.org/protobuf/cmd/protoc-gen-go@latest 7 | go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest 8 | 9 | buf: 10 | pnpm buf 11 | 12 | format: 13 | just --fmt --unstable 14 | -------------------------------------------------------------------------------- /packages/proto/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@goat/proto", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "buf": "buf generate" 6 | }, 7 | "devDependencies": { 8 | "@bufbuild/buf":"1.50.0", 9 | "@bufbuild/protoc-gen-es": "2.2.4", 10 | "@connectrpc/protoc-gen-connect-query": "2.0.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/typescript-config/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationMap": true, 6 | "esModuleInterop": true, 7 | "incremental": false, 8 | "isolatedModules": true, 9 | "lib": ["es2022", "DOM", "DOM.Iterable"], 10 | "module": "NodeNext", 11 | "moduleDetection": "force", 12 | "moduleResolution": "NodeNext", 13 | "noUncheckedIndexedAccess": true, 14 | "resolveJsonModule": true, 15 | "skipLibCheck": true, 16 | "strict": true, 17 | "target": "ES2022" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/typescript-config/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./base.json", 4 | "compilerOptions": { 5 | "plugins": [{ "name": "next" }], 6 | "module": "ESNext", 7 | "moduleResolution": "Bundler", 8 | "allowJs": true, 9 | "jsx": "preserve", 10 | "noEmit": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/typescript-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@goat/typescript-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "publishConfig": { 7 | "access": "public" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/typescript-config/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./base.json", 4 | "compilerOptions": { 5 | "jsx": "react-jsx" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { config } from "@goat/eslint-config/react-internal"; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@goat/ui", 3 | "version": "0.0.0", 4 | "sideEffects": [ 5 | "**/*.css" 6 | ], 7 | "files": [ 8 | "dist" 9 | ], 10 | "exports": { 11 | "./styles.css": "./dist/index.css", 12 | "./components/*": "./src/components/*" 13 | }, 14 | "license": "MIT", 15 | "scripts": { 16 | "check-types": "tsc --noEmit" 17 | }, 18 | "peerDependencies": { 19 | "react": "^19" 20 | }, 21 | "devDependencies": { 22 | "@goat/typescript-config": "workspace:*", 23 | "@types/react": "19.0.7", 24 | "autoprefixer": "10.4.20", 25 | "postcss": "8.5.1", 26 | "typescript": "5.7.3" 27 | }, 28 | "dependencies": { 29 | "@radix-ui/react-slot": "1.1.2", 30 | "@tailwindcss/postcss": "4.0.14", 31 | "class-variance-authority": "0.7.1", 32 | "clsx": "2.1.1", 33 | "recharts": "2.15.1", 34 | "tailwind-merge": "3.0.2", 35 | "tailwindcss": "4.0.6", 36 | "tailwindcss-animate": "1.0.7" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/ui/src/components/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "../lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /packages/ui/src/components/card.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | export function Card({ 4 | title, 5 | children, 6 | href, 7 | }: { 8 | title: string; 9 | children: ReactNode; 10 | href: string; 11 | }) { 12 | return ( 13 | 19 |

20 | {title}{" "} 21 | 22 | -> 23 | 24 |

25 |

26 | {children} 27 |

28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /packages/ui/src/components/charts.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as RechartsPrimitive from "recharts" 5 | 6 | import { cn } from "../lib/utils" 7 | 8 | // Format: { THEME_NAME: CSS_SELECTOR } 9 | const THEMES = { light: "", dark: ".dark" } as const 10 | 11 | export type ChartConfig = { 12 | [k in string]: { 13 | label?: React.ReactNode 14 | icon?: React.ComponentType 15 | } & ( 16 | | { color?: string; theme?: never } 17 | | { color?: never; theme: Record } 18 | ) 19 | } 20 | 21 | type ChartContextProps = { 22 | config: ChartConfig 23 | } 24 | 25 | const ChartContext = React.createContext(null) 26 | 27 | function useChart() { 28 | const context = React.useContext(ChartContext) 29 | 30 | if (!context) { 31 | throw new Error("useChart must be used within a ") 32 | } 33 | 34 | return context 35 | } 36 | 37 | const ChartContainer = React.forwardRef< 38 | HTMLDivElement, 39 | React.ComponentProps<"div"> & { 40 | config: ChartConfig 41 | children: React.ComponentProps< 42 | typeof RechartsPrimitive.ResponsiveContainer 43 | >["children"] 44 | } 45 | >(({ id, className, children, config, ...props }, ref) => { 46 | const uniqueId = React.useId() 47 | const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` 48 | 49 | return ( 50 | 51 |
60 | 61 | 62 | {children} 63 | 64 |
65 |
66 | ) 67 | }) 68 | ChartContainer.displayName = "Chart" 69 | 70 | const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { 71 | const colorConfig = Object.entries(config).filter( 72 | ([, config]) => config.theme || config.color 73 | ) 74 | 75 | if (!colorConfig.length) { 76 | return null 77 | } 78 | 79 | return ( 80 |