├── .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 |
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 |
82 | Home
83 | About
84 |
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 | countStore.setState((n) => n + 1)}>
236 | Increment - {count}
237 |
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 |
countStore.setState((n) => n + 1)}>
269 | Increment - {count}
270 |
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 |
{
24 | await v.mutateAsync({
25 | Vote: Vote.YES,
26 | });
27 | navigate({ to: "/results" });
28 | }}
29 | >
30 | Yes
31 |
32 |
{
36 | await v.mutateAsync({
37 | Vote: Vote.NO,
38 | });
39 | navigate({ to: "/results" });
40 | }}
41 | >
42 | No
43 |
44 |
45 | {" "}
46 |
47 | View results
48 |
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 |