initial commit
This commit is contained in:
commit
3640116dd5
1
.dockerignore
Normal file
1
.dockerignore
Normal file
@ -0,0 +1 @@
|
||||
/build
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/build
|
||||
/internal/web/static
|
22
Dockerfile
Normal file
22
Dockerfile
Normal file
@ -0,0 +1,22 @@
|
||||
FROM golang:1.23-alpine AS build
|
||||
|
||||
ADD . /app
|
||||
WORKDIR /app
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETOS
|
||||
|
||||
RUN apk add --no-cache make npm
|
||||
RUN make deps
|
||||
RUN make BUILD_ARCH="$TARGETARCH" BUILD_OS="$TARGETOS" build
|
||||
|
||||
FROM alpine:latest
|
||||
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
WORKDIR /data
|
||||
COPY --from=build /app/build/beerpong-elo /app/beerpong-elo
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD [ "/app/beerpong-elo" ]
|
44
Makefile
Normal file
44
Makefile
Normal file
@ -0,0 +1,44 @@
|
||||
BINARY = beerpong-elo
|
||||
BUILD_DIR = build
|
||||
BUILD_ARCH = $(shell go env GOARCH)
|
||||
BUILD_OS = $(shell go env GOOS)
|
||||
DOCKER_BUILD_FLAGS =
|
||||
DOCKER_BUILD_PLATFORM = linux/amd64,linux/arm64,linux/arm32v6,linux/arm32v7
|
||||
DOCKER_BUILD_TAG = djeeberjr/beerpong-elo
|
||||
DIST_DIR = static
|
||||
EMBED_DIR = internal/web/static
|
||||
|
||||
.PHONY: all
|
||||
all: clean build
|
||||
|
||||
.PHONY:build
|
||||
build: $(BUILD_DIR)/$(BINARY)
|
||||
|
||||
$(BUILD_DIR)/$(BINARY): $(EMBED_DIR) $(shell find . -name '*.go')
|
||||
GOARCH=$(BUILD_ARCH) GOOS=$(BUILD_OS) go build -o $(BUILD_DIR)/$(BINARY) cmd/beerpong-elo.go
|
||||
|
||||
$(EMBED_DIR): $(BUILD_DIR)/$(DIST_DIR)
|
||||
cp -r $(BUILD_DIR)/$(DIST_DIR) $(EMBED_DIR)
|
||||
|
||||
$(BUILD_DIR)/$(DIST_DIR):
|
||||
cd web/ && npm run build && mkdir ../$(BUILD_DIR) && cp -r dist/ ../$(BUILD_DIR)/$(DIST_DIR)
|
||||
|
||||
.PHONY: deps
|
||||
deps:
|
||||
cd web/ && npm ci
|
||||
|
||||
.PHONY:clean
|
||||
clean:
|
||||
rm -rf $(BUILD_DIR) $(EMBED_DIR)
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
go test ./...
|
||||
|
||||
.PHONY: docker-push
|
||||
docker-push:
|
||||
docker buildx build $(DOCKER_BUILD_FLAGS) --platform $(DOCKER_BUILD_PLATFORM) -t $(DOCKER_BUILD_TAG) . --push
|
||||
|
||||
.PHONY: docker-local
|
||||
docker-local:
|
||||
docker buildx build $(DOCKER_BUILD_FLAGS) -t $(DOCKER_BUILD_TAG) . --load
|
12
README.md
Normal file
12
README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Beerpong Elo
|
||||
|
||||
An ELO system for 2v2 beerpong with web interface and graphql api. Using Svelte 5 for the frontend.
|
||||
|
||||
# Building
|
||||
Requires go
|
||||
|
||||
`make build`
|
||||
|
||||
## Docker
|
||||
|
||||
Building the docker image: `make docker` and `make docker-local`.
|
39
cmd/beerpong-elo.go
Normal file
39
cmd/beerpong-elo.go
Normal file
@ -0,0 +1,39 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
beerpongelo "git.kapelle.org/niklas/beerpong-elo/internal"
|
||||
"github.com/alexflint/go-arg"
|
||||
)
|
||||
|
||||
type args struct {
|
||||
DBHost string `arg:"--db-endpoint,required,env:DB_HOST" help:"IP or hostname of the DB" placeholder:"DB_HOST"`
|
||||
DBUsername string `arg:"--db-username,required,env:DB_USERNAME" help:"Username for the DB" placeholder:"DB_USERNAME"`
|
||||
DBPassword string `arg:"--db-password,required,env:DB_PASSWORD" help:"Password for the DB" placeholder:"DB_PASSWORD"`
|
||||
DBName string `arg:"--db-name,required,env:DB_NAME" help:"Name of the DB" placeholder:"DB_NAME"`
|
||||
Address string `arg:"--address,env:ADDRESS" default:":3000" help:"What address to listen on" placeholder:"ADDRESS"`
|
||||
Import string `arg:"--import" help:"Import a json file and exit" placeholder:"data.json"`
|
||||
}
|
||||
|
||||
func (args) Version() string {
|
||||
return "beerpong-elo v0.1"
|
||||
}
|
||||
|
||||
func main() {
|
||||
var args args
|
||||
arg.MustParse(&args)
|
||||
|
||||
config := beerpongelo.Config{
|
||||
DBHost: args.DBHost,
|
||||
DBUsername: args.DBUsername,
|
||||
DBPassword: args.DBPassword,
|
||||
DBName: args.DBName,
|
||||
Address: args.Address,
|
||||
}
|
||||
|
||||
if args.Import != "" {
|
||||
beerpongelo.Import(config,args.Import)
|
||||
return
|
||||
}
|
||||
|
||||
beerpongelo.Start(config)
|
||||
}
|
17
go.mod
Normal file
17
go.mod
Normal file
@ -0,0 +1,17 @@
|
||||
module git.kapelle.org/niklas/beerpong-elo
|
||||
|
||||
go 1.22.6
|
||||
|
||||
require github.com/alexflint/go-arg v1.5.1
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/alexflint/go-scalar v1.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/graphql-go/graphql v0.8.1 // indirect
|
||||
github.com/graphql-go/handler v0.2.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
26
go.sum
Normal file
26
go.sum
Normal file
@ -0,0 +1,26 @@
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/alexflint/go-arg v1.5.1 h1:nBuWUCpuRy0snAG+uIJ6N0UvYxpxA0/ghA/AaHxlT8Y=
|
||||
github.com/alexflint/go-arg v1.5.1/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
|
||||
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
||||
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/graphql-go/graphql v0.8.1 h1:p7/Ou/WpmulocJeEx7wjQy611rtXGQaAcXGqanuMMgc=
|
||||
github.com/graphql-go/graphql v0.8.1/go.mod h1:nKiHzRM0qopJEwCITUuIsxk9PlVlwIiiI8pnJEhordQ=
|
||||
github.com/graphql-go/handler v0.2.4 h1:gz9q11TUHPNUpqzV8LMa+rkqM5NUuH/nkE3oF2LS3rI=
|
||||
github.com/graphql-go/handler v0.2.4/go.mod h1:gsQlb4gDvURR0bgN8vWQEh+s5vJALM2lYL3n3cf6OxQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
71
internal/beerpong-elo.go
Normal file
71
internal/beerpong-elo.go
Normal file
@ -0,0 +1,71 @@
|
||||
package beerpongelo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"git.kapelle.org/niklas/beerpong-elo/internal/model"
|
||||
"git.kapelle.org/niklas/beerpong-elo/internal/repo"
|
||||
"git.kapelle.org/niklas/beerpong-elo/internal/web"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
DBHost string
|
||||
DBUsername string
|
||||
DBPassword string
|
||||
DBName string
|
||||
Address string
|
||||
}
|
||||
|
||||
func Start(config Config) {
|
||||
repo, err := repo.NewSQLRepo(config.DBHost, config.DBUsername, config.DBPassword, config.DBName)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to start server: %s", err)
|
||||
}
|
||||
|
||||
mux := web.CreateWebserver(repo)
|
||||
|
||||
http.ListenAndServe(config.Address, mux)
|
||||
}
|
||||
|
||||
func Import(config Config, file string) {
|
||||
repo, err := repo.NewSQLRepo(config.DBHost, config.DBUsername, config.DBPassword, config.DBName)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to import file %s: %s", file, err)
|
||||
}
|
||||
|
||||
controller := newController(repo)
|
||||
err = loadFromFile(controller, file)
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to import file %s: %s", file, err)
|
||||
}
|
||||
}
|
||||
|
||||
func loadFromFile(c Controller, path string) error {
|
||||
content, err := os.ReadFile(path)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var payload []model.Game
|
||||
err = json.Unmarshal(content, &payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, game := range payload {
|
||||
_, err = c.AddGame(game)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Imported %d games", len(payload))
|
||||
return nil
|
||||
}
|
142
internal/controller.go
Normal file
142
internal/controller.go
Normal file
@ -0,0 +1,142 @@
|
||||
package beerpongelo
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"git.kapelle.org/niklas/beerpong-elo/internal/model"
|
||||
"git.kapelle.org/niklas/beerpong-elo/internal/repo"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
repo repo.Repo
|
||||
}
|
||||
|
||||
func newController(repo repo.Repo) Controller {
|
||||
return Controller{
|
||||
repo: repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) AddGame(game model.Game) (model.GameID, error) {
|
||||
parsedGame := model.Game{
|
||||
Added: game.Added,
|
||||
Score: game.Score,
|
||||
Overtime: game.Overtime,
|
||||
}
|
||||
|
||||
author, err := c.repo.GetOrCreatePlayerID(string(game.Author))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parsedGame.Author = author
|
||||
|
||||
t0p0, err := c.repo.GetOrCreatePlayerID(string(game.Team0Player0))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parsedGame.Team0Player0 = t0p0
|
||||
|
||||
t0p1, err := c.repo.GetOrCreatePlayerID(string(game.Team0Player1))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parsedGame.Team0Player1 = t0p1
|
||||
|
||||
t1p0, err := c.repo.GetOrCreatePlayerID(string(game.Team1Player0))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parsedGame.Team1Player0 = t1p0
|
||||
|
||||
t1p1, err := c.repo.GetOrCreatePlayerID(string(game.Team1Player1))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parsedGame.Team1Player1 = t1p1
|
||||
|
||||
id, err := c.repo.AddGame(parsedGame)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
parsedGame.ID = id
|
||||
|
||||
err = c.createGameResult(parsedGame)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (c *Controller) createGameResult(game model.Game) error {
|
||||
t0p0, err := c.repo.GetPlayer(game.Team0Player0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t0p1, err := c.repo.GetPlayer(game.Team0Player1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t1p0, err := c.repo.GetPlayer(game.Team1Player0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t1p1, err := c.repo.GetPlayer(game.Team1Player1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newT0p0, newT0p1, newT1p0, newT1p1 := NewRating(float64(t0p0.Elo), float64(t0p1.Elo), float64(t1p0.Elo), float64(t1p1.Elo), game.Score)
|
||||
|
||||
t0p0Result := model.GameResult{
|
||||
Game: game.ID,
|
||||
Player: t0p0.ID,
|
||||
StartElo: t0p0.Elo,
|
||||
EndElo: int(math.Round(newT0p0)),
|
||||
}
|
||||
|
||||
t0p1Result := model.GameResult{
|
||||
Game: game.ID,
|
||||
Player: t0p1.ID,
|
||||
StartElo: t0p1.Elo,
|
||||
EndElo: int(math.Round(newT0p1)),
|
||||
}
|
||||
|
||||
t1p0Result := model.GameResult{
|
||||
Game: game.ID,
|
||||
Player: t1p0.ID,
|
||||
StartElo: t1p0.Elo,
|
||||
EndElo: int(math.Round(newT1p0)),
|
||||
}
|
||||
|
||||
t1p1Result := model.GameResult{
|
||||
Game: game.ID,
|
||||
Player: t1p1.ID,
|
||||
StartElo: t1p1.Elo,
|
||||
EndElo: int(math.Round(newT1p1)),
|
||||
}
|
||||
|
||||
_,err = c.repo.AddGameResult(t0p0Result)
|
||||
if err != nil{
|
||||
return err
|
||||
}
|
||||
|
||||
_,err = c.repo.AddGameResult(t0p1Result)
|
||||
if err != nil{
|
||||
return err
|
||||
}
|
||||
|
||||
_,err = c.repo.AddGameResult(t1p0Result)
|
||||
if err != nil{
|
||||
return err
|
||||
}
|
||||
|
||||
_,err = c.repo.AddGameResult(t1p1Result)
|
||||
if err != nil{
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
52
internal/elo.go
Normal file
52
internal/elo.go
Normal file
@ -0,0 +1,52 @@
|
||||
package beerpongelo
|
||||
|
||||
import (
|
||||
"math"
|
||||
)
|
||||
|
||||
func winProbPlayer(p, e1, e2 float64) float64 {
|
||||
const MAGIC float64 = 500
|
||||
|
||||
lhs := 1 / (1 + math.Pow(10., (p-e1)/MAGIC))
|
||||
rhs := 1 / (1 + math.Pow(10., (p-e2)/MAGIC))
|
||||
return (lhs + rhs) / 2.
|
||||
}
|
||||
|
||||
func NewRating(t0p0, t0p1, t1p0, t1p1 float64, score int) (float64, float64, float64, float64) {
|
||||
const K float64 = 50
|
||||
const SCORE_BIAS float64 = 1
|
||||
const SCORE_SCALE float64 = 0.4
|
||||
const MAX_SCORE float64 = 10
|
||||
|
||||
et0p0 := winProbPlayer(t0p0, t1p0, t1p1)
|
||||
et0p1 := winProbPlayer(t0p1, t1p0, t1p1)
|
||||
et1p0 := winProbPlayer(t1p0, t0p0, t0p1)
|
||||
et1p1 := winProbPlayer(t1p1, t0p0, t0p1)
|
||||
|
||||
et0 := (et0p0 + et0p1) / 2.
|
||||
et1 := (et1p0 + et1p1) / 2.
|
||||
|
||||
ct0p0 := t0p0 / (t0p0 + t0p1)
|
||||
ct0p1 := t0p1 / (t0p0 + t0p1)
|
||||
ct1p0 := t1p0 / (t1p0 + t1p1)
|
||||
ct1p1 := t1p1 / (t1p0 + t1p1)
|
||||
|
||||
var st0 float64
|
||||
var st1 float64
|
||||
if score > 0 {
|
||||
st0 = 1
|
||||
st1 = 0
|
||||
} else {
|
||||
st0 = 0
|
||||
st1 = 1
|
||||
}
|
||||
|
||||
scoreMod := SCORE_SCALE * math.Log(math.Abs(float64(score))+SCORE_BIAS) / (math.Log(MAX_SCORE) + SCORE_BIAS)
|
||||
|
||||
newt0p0 := t0p0 + K*scoreMod*((st0*ct0p0)-(et0*ct0p0))
|
||||
newt0p1 := t0p1 + K*scoreMod*((st0*ct0p1)-(et0*ct0p1))
|
||||
newt1p0 := t1p0 + K*scoreMod*((st1*ct1p0)-(et1*ct1p0))
|
||||
newt1p1 := t1p1 + K*scoreMod*((st1*ct1p1)-(et1*ct1p1))
|
||||
|
||||
return newt0p0, newt0p1, newt1p0, newt1p1
|
||||
}
|
33
internal/model/game.go
Normal file
33
internal/model/game.go
Normal file
@ -0,0 +1,33 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GameID string
|
||||
|
||||
type Game struct {
|
||||
ID GameID `json:"id"`
|
||||
Added time.Time `json:"added"`
|
||||
Author PlayerID `json:"author"`
|
||||
Team0Player0 PlayerID `json:"t0p0"`
|
||||
Team0Player1 PlayerID `json:"t0p1"`
|
||||
Team1Player0 PlayerID `json:"t1p0"`
|
||||
Team1Player1 PlayerID `json:"t1p1"`
|
||||
Score int `json:"score"`
|
||||
Overtime bool `jsone:"ot"`
|
||||
}
|
||||
|
||||
func (id *GameID) Scan(src any) error {
|
||||
switch v := src.(type) {
|
||||
case int64:
|
||||
*id = GameID(fmt.Sprintf("%d", v))
|
||||
return nil
|
||||
case string:
|
||||
*id = GameID(v)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported type %T for GameID", src)
|
||||
}
|
||||
}
|
26
internal/model/gameResult.go
Normal file
26
internal/model/gameResult.go
Normal file
@ -0,0 +1,26 @@
|
||||
package model
|
||||
|
||||
import "fmt"
|
||||
|
||||
type GameResultID string
|
||||
|
||||
type GameResult struct {
|
||||
ID GameResultID
|
||||
Game GameID
|
||||
Player PlayerID
|
||||
StartElo int
|
||||
EndElo int
|
||||
}
|
||||
|
||||
func (id *GameResultID) Scan(src any) error {
|
||||
switch v := src.(type) {
|
||||
case int64:
|
||||
*id = GameResultID(fmt.Sprintf("%d", v)) // Convert int64 to string and assign it
|
||||
return nil
|
||||
case string:
|
||||
*id = GameResultID(v)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported type %T for GameResultID", src)
|
||||
}
|
||||
}
|
31
internal/model/player.go
Normal file
31
internal/model/player.go
Normal file
@ -0,0 +1,31 @@
|
||||
package model
|
||||
|
||||
import "fmt"
|
||||
|
||||
type PlayerID string
|
||||
|
||||
type Player struct {
|
||||
ID PlayerID
|
||||
Name string
|
||||
Elo int
|
||||
}
|
||||
|
||||
func NewPlayer(name string) Player {
|
||||
return Player{
|
||||
Name: name,
|
||||
Elo: 1000,
|
||||
}
|
||||
}
|
||||
|
||||
func (id *PlayerID) Scan(src any) error {
|
||||
switch v := src.(type) {
|
||||
case int64:
|
||||
*id = PlayerID(fmt.Sprintf("%d", v)) // Convert int64 to string and assign it
|
||||
return nil
|
||||
case string:
|
||||
*id = PlayerID(v)
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported type %T for PlayerID", src)
|
||||
}
|
||||
}
|
40
internal/repo/repo.go
Normal file
40
internal/repo/repo.go
Normal file
@ -0,0 +1,40 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
model "git.kapelle.org/niklas/beerpong-elo/internal/model"
|
||||
)
|
||||
|
||||
type Repo interface {
|
||||
// Add a new game to the repo. ID needs to be nil on argument. PlayerID are fetched beforehand.
|
||||
AddGame(model.Game) (model.GameID, error)
|
||||
|
||||
// Get a game by ID. Returns nil if not found.
|
||||
GetGame(model.GameID) (*model.Game, error)
|
||||
|
||||
// Get ID of a player. Creates one if not found.
|
||||
GetOrCreatePlayerID(string) (model.PlayerID, error)
|
||||
|
||||
// Get player by id. Returns nil if not found.
|
||||
GetPlayer(model.PlayerID) (*model.Player, error)
|
||||
|
||||
// Adds a game result. ID needs to be nil on argument.
|
||||
AddGameResult(model.GameResult) (model.GameResultID, error)
|
||||
|
||||
// Get a game result by ID. Returns nil if not found.
|
||||
GetGameResult(model.GameResultID) (*model.GameResult, error)
|
||||
|
||||
// Get a game result for a player in a game
|
||||
GetGameResultForPlayerAndGame(model.GameID, model.PlayerID) (*model.GameResult, error)
|
||||
|
||||
// Get all game results for a player
|
||||
GetGameResultsForPlayer(model.PlayerID) ([]*model.GameResult, error)
|
||||
|
||||
// Get games for a player
|
||||
GetGamesForPlayer(model.PlayerID) ([]*model.Game, error)
|
||||
|
||||
// Get all players
|
||||
GetPlayers() ([]*model.Player, error)
|
||||
|
||||
// Get all games
|
||||
GetGames() ([]*model.Game, error)
|
||||
}
|
279
internal/repo/sqlRepo.go
Normal file
279
internal/repo/sqlRepo.go
Normal file
@ -0,0 +1,279 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.kapelle.org/niklas/beerpong-elo/internal/model"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
type SQLRepo struct {
|
||||
db *sql.DB
|
||||
}
|
||||
|
||||
func NewSQLRepo(host, username, password, dbName string) (Repo, error) {
|
||||
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s?parseTime=true", username, password, host, dbName))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
db.SetConnMaxLifetime(time.Minute * 3)
|
||||
db.SetMaxOpenConns(10)
|
||||
db.SetMaxIdleConns(10)
|
||||
|
||||
return &SQLRepo{
|
||||
db: db,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) AddGame(game model.Game) (model.GameID, error) {
|
||||
stmt, err := s.db.Prepare("INSERT INTO Games(added,score,overtime,author,team0player0,team0player1,team1player0,team1player1) VALUES (?,?,?,?,?,?,?,?)")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
res, err := stmt.Exec(game.Added, game.Score, game.Overtime, game.Author, game.Team0Player0, game.Team0Player1, game.Team1Player0, game.Team1Player1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var gameID model.GameID
|
||||
gameID.Scan(id)
|
||||
|
||||
return gameID, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) AddGameResult(gameResult model.GameResult) (model.GameResultID, error) {
|
||||
stms, err := s.db.Prepare("INSERT INTO GameResults(game,player,startElo,endElo) VALUES (?,?,?,?)")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
res, err := stms.Exec(gameResult.Game, gameResult.Player, gameResult.StartElo, gameResult.EndElo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var gameResultID model.GameResultID
|
||||
gameResultID.Scan(id)
|
||||
|
||||
return gameResultID, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) GetGame(id model.GameID) (*model.Game, error) {
|
||||
rows := s.db.QueryRow("SELECT id,added, score,overtime,author,team0player0,team0player1,team1player0,team1player1 FROM Games WHERE id = ? ", id)
|
||||
|
||||
var game model.Game
|
||||
|
||||
if err := rows.Scan(&game.ID, &game.Added, &game.Score, &game.Overtime, &game.Author, &game.Team0Player0, &game.Team0Player1, &game.Team1Player0, &game.Team1Player1); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &game, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) GetGameResult(id model.GameResultID) (*model.GameResult, error) {
|
||||
rows := s.db.QueryRow("SELECT game,player,startElo,endElo FROM GameResults WHERE id = ? ", id)
|
||||
|
||||
var gameResult model.GameResult
|
||||
gameResult.ID = id
|
||||
|
||||
if err := rows.Scan(&gameResult.Game, &gameResult.Player, &gameResult.StartElo, &gameResult.EndElo); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gameResult, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) playerWithNameExists(name string) (*model.PlayerID, error) {
|
||||
rows := s.db.QueryRow("SELECT id FROM Players WHERE name = ?", name)
|
||||
|
||||
var id model.PlayerID
|
||||
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &id, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) GetOrCreatePlayerID(name string) (model.PlayerID, error) {
|
||||
foundID, err := s.playerWithNameExists(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if foundID != nil {
|
||||
return *foundID, nil
|
||||
}
|
||||
|
||||
stmt, err := s.db.Prepare("INSERT INTO Players(name,elo) VALUES (?,?)")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
newPlayer := model.NewPlayer(name)
|
||||
|
||||
res, err := stmt.Exec(newPlayer.Name, newPlayer.Elo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var playerID model.PlayerID
|
||||
playerID.Scan(id)
|
||||
|
||||
return playerID, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) GetPlayer(id model.PlayerID) (*model.Player, error) {
|
||||
rows := s.db.QueryRow("SELECT name,elo from Players WHERE id = ?", id)
|
||||
|
||||
var player model.Player
|
||||
player.ID = id
|
||||
|
||||
if err := rows.Scan(&player.Name, &player.Elo); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &player, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) GetGamesForPlayer(id model.PlayerID) ([]*model.Game, error) {
|
||||
rows, err := s.db.Query(`
|
||||
SELECT id,added, score,overtime,author,team0player0,team0player1,team1player0,team1player1
|
||||
FROM Games
|
||||
WHERE team0player0 = ? OR team0player1 = ? OR team1player0 = ? OR team1player1 = ?
|
||||
`, id, id, id, id)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer rows.Close()
|
||||
|
||||
rtn := []*model.Game{}
|
||||
|
||||
for rows.Next() {
|
||||
var game model.Game
|
||||
err = rows.Scan(&game.ID, &game.Added, &game.Score, &game.Overtime, &game.Author, &game.Team0Player0, &game.Team0Player1, &game.Team1Player0, &game.Team1Player1)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtn = append(rtn, &game)
|
||||
}
|
||||
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) GetGameResultForPlayerAndGame(gameID model.GameID, playerID model.PlayerID) (*model.GameResult, error) {
|
||||
rows := s.db.QueryRow("SELECT id,game,player,startElo,endElo FROM GameResults WHERE game = ? AND player = ? ", gameID, playerID)
|
||||
|
||||
var gameResult model.GameResult
|
||||
|
||||
if err := rows.Scan(&gameResult.ID, &gameResult.Game, &gameResult.Player, &gameResult.StartElo, &gameResult.EndElo); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &gameResult, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) GetGameResultsForPlayer(id model.PlayerID) ([]*model.GameResult, error) {
|
||||
rows, err := s.db.Query("SELECT id,game,player,startElo,endElo FROM GameResults WHERE player = ? ", id)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtn := []*model.GameResult{}
|
||||
|
||||
for rows.Next() {
|
||||
var gameResult model.GameResult
|
||||
|
||||
err = rows.Scan(&gameResult.ID, &gameResult.Game, &gameResult.Player, &gameResult.StartElo, &gameResult.EndElo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtn = append(rtn, &gameResult)
|
||||
}
|
||||
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) GetPlayers() ([]*model.Player, error) {
|
||||
rows, err := s.db.Query("SELECT id, name, elo FROM Players")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtn := []*model.Player{}
|
||||
|
||||
for rows.Next() {
|
||||
var player model.Player
|
||||
|
||||
err = rows.Scan(&player.ID, &player.Name, &player.Elo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtn = append(rtn, &player)
|
||||
}
|
||||
|
||||
return rtn, nil
|
||||
}
|
||||
|
||||
func (s *SQLRepo) GetGames() ([]*model.Game, error) {
|
||||
rows, err := s.db.Query("SELECT id,added, score,overtime,author,team0player0,team0player1,team1player0,team1player1 FROM Games")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtn := []*model.Game{}
|
||||
|
||||
for rows.Next() {
|
||||
var game model.Game
|
||||
err = rows.Scan(&game.ID, &game.Added, &game.Score, &game.Overtime, &game.Author, &game.Team0Player0, &game.Team0Player1, &game.Team1Player0, &game.Team1Player1)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
rtn = append(rtn, &game)
|
||||
}
|
||||
|
||||
return rtn, nil
|
||||
}
|
44
internal/repo/sqlSchema.sql
Normal file
44
internal/repo/sqlSchema.sql
Normal file
@ -0,0 +1,44 @@
|
||||
CREATE TABLE Players (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
elo INT
|
||||
);
|
||||
|
||||
CREATE TABLE Games (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
added DATETIME NOT NULL,
|
||||
score INT NOT NULL,
|
||||
overtime BOOL NOT NULL,
|
||||
author INT,
|
||||
team0player0 INT,
|
||||
team0player1 INT,
|
||||
team1player0 INT,
|
||||
team1player1 INT,
|
||||
FOREIGN KEY (author) REFERENCES Players(id) on DELETE SET NULL,
|
||||
FOREIGN KEY (team0player0) REFERENCES Players(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (team0player1) REFERENCES Players(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (team1player0) REFERENCES Players(id) ON DELETE SET NULL,
|
||||
FOREIGN KEY (team1player1) REFERENCES Players(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE GameResults (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
game INT,
|
||||
player INT,
|
||||
startElo INT,
|
||||
endElo INT,
|
||||
FOREIGN KEY (game) REFERENCES Games(id) on DELETE SET NULL,
|
||||
FOREIGN KEY (player) REFERENCES Players(id) on DELETE SET NULL
|
||||
)
|
||||
|
||||
DELIMITER $$
|
||||
CREATE TRIGGER UpdateCurrentELO
|
||||
AFTER INSERT ON GameResults
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
UPDATE Players
|
||||
SET elo = NEW.endElo
|
||||
WHERE id = NEW.player;
|
||||
END$$
|
||||
|
||||
DELIMITER ;
|
369
internal/web/gqlSchema.go
Normal file
369
internal/web/gqlSchema.go
Normal file
@ -0,0 +1,369 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"git.kapelle.org/niklas/beerpong-elo/internal/model"
|
||||
"git.kapelle.org/niklas/beerpong-elo/internal/repo"
|
||||
"github.com/graphql-go/graphql"
|
||||
)
|
||||
|
||||
func createShema(repo repo.Repo) graphql.Schema {
|
||||
|
||||
player := graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "Player",
|
||||
Description: "A player. Can also be authors of games",
|
||||
Fields: graphql.Fields{
|
||||
"ID": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.ID),
|
||||
},
|
||||
"name": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.String),
|
||||
},
|
||||
"elo": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.Int),
|
||||
},
|
||||
|
||||
// Field "games" added below
|
||||
// Field "history" added below
|
||||
},
|
||||
})
|
||||
|
||||
game := graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "Game",
|
||||
Description: "A game played",
|
||||
Fields: graphql.Fields{
|
||||
"id": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.ID),
|
||||
},
|
||||
"added": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.DateTime),
|
||||
},
|
||||
"overtime": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.Boolean),
|
||||
},
|
||||
"score": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.Int),
|
||||
},
|
||||
"author": &graphql.Field{
|
||||
Type: graphql.NewNonNull(player),
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
game, ok := p.Source.(*model.Game)
|
||||
if !ok {
|
||||
log.Printf("Can't get source for field 'author' on 'Game' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
player, err := repo.GetPlayer(game.Author)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return player, nil
|
||||
},
|
||||
},
|
||||
"team0player0": &graphql.Field{
|
||||
Type: graphql.NewNonNull(player),
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
game, ok := p.Source.(*model.Game)
|
||||
if !ok {
|
||||
log.Printf("Can't get source for field 'team0player0' on 'Game' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
player, err := repo.GetPlayer(game.Team0Player0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return player, nil
|
||||
},
|
||||
},
|
||||
"team0player1": &graphql.Field{
|
||||
Type: graphql.NewNonNull(player),
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
game, ok := p.Source.(*model.Game)
|
||||
if !ok {
|
||||
log.Printf("Can't get source for field 'team0player1' on 'Game' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
player, err := repo.GetPlayer(game.Team0Player1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return player, nil
|
||||
},
|
||||
},
|
||||
"team1player0": &graphql.Field{
|
||||
Type: graphql.NewNonNull(player),
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
game, ok := p.Source.(*model.Game)
|
||||
if !ok {
|
||||
log.Printf("Can't get source for field 'team1player0' on 'Game' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
player, err := repo.GetPlayer(game.Team1Player0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return player, nil
|
||||
},
|
||||
},
|
||||
"team1player1": &graphql.Field{
|
||||
Type: graphql.NewNonNull(player),
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
game, ok := p.Source.(*model.Game)
|
||||
if !ok {
|
||||
log.Printf("Can't get source for field 'team1player1' on 'Game' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
player, err := repo.GetPlayer(game.Team1Player1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return player, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
player.AddFieldConfig("games", &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.NewList(game)),
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
player, ok := p.Source.(*model.Player)
|
||||
|
||||
if !ok {
|
||||
log.Printf("Can't get source for field 'games' on 'Player' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
games, err := repo.GetGamesForPlayer(player.ID)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return games, nil
|
||||
},
|
||||
})
|
||||
|
||||
gameResult := graphql.NewObject(graphql.ObjectConfig{
|
||||
Name: "GameResult",
|
||||
Description: "The ELO change for a player in a game",
|
||||
Fields: graphql.Fields{
|
||||
"id": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.ID),
|
||||
},
|
||||
"player": &graphql.Field{
|
||||
Type: player,
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
gameResult, ok := p.Source.(*model.GameResult)
|
||||
if !ok {
|
||||
log.Printf("Can't get source for field 'player' on 'GameResult' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
player, err := repo.GetPlayer(gameResult.Player)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return player, nil
|
||||
},
|
||||
},
|
||||
"game": &graphql.Field{
|
||||
Type: graphql.NewNonNull(game),
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
gameResult, ok := p.Source.(*model.GameResult)
|
||||
if !ok {
|
||||
log.Printf("Can't get source for field 'game' on 'GameResult' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
game, err := repo.GetGame(gameResult.Game)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return game, nil
|
||||
},
|
||||
},
|
||||
"startElo": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.Int),
|
||||
},
|
||||
"endElo": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.Int),
|
||||
},
|
||||
"delta": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.Int),
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
gameResult, ok := p.Source.(*model.GameResult)
|
||||
if !ok {
|
||||
log.Printf("Can't get source for field 'delta' on 'GameResult' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
delta := gameResult.EndElo - gameResult.StartElo
|
||||
return delta, nil
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
player.AddFieldConfig("history", &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.NewList(gameResult)),
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"game": &graphql.ArgumentConfig{
|
||||
Type: graphql.ID,
|
||||
Description: "Filter by Game ID",
|
||||
},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
player, ok := p.Source.(*model.Player)
|
||||
if !ok {
|
||||
log.Printf("Can't get source from field 'history' on 'player' is %v", p.Source)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if p.Args["game"] != nil {
|
||||
// Find result for single game
|
||||
gameID, ok := p.Args["game"].(string)
|
||||
if !ok {
|
||||
log.Printf("Failed to parse 'game' at field 'history' on 'player' is %v", p.Args["game"])
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
gameResult, err := repo.GetGameResultForPlayerAndGame(model.GameID(gameID), player.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return []*model.GameResult{gameResult}, nil
|
||||
}
|
||||
|
||||
// Find all gameResults for a player
|
||||
|
||||
gameResults, err := repo.GetGameResultsForPlayer(player.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gameResults, nil
|
||||
},
|
||||
})
|
||||
|
||||
queryFields := graphql.Fields{
|
||||
"player": &graphql.Field{
|
||||
Type: player,
|
||||
Description: "Get player by ID",
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"id": &graphql.ArgumentConfig{
|
||||
Type: graphql.NewNonNull(graphql.ID),
|
||||
},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
id, ok := p.Args["id"].(string)
|
||||
if !ok {
|
||||
log.Printf("Failed to parse ID at player: %v", p.Args["id"])
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
player, err := repo.GetPlayer(model.PlayerID(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return player, nil
|
||||
},
|
||||
},
|
||||
"game": &graphql.Field{
|
||||
Type: game,
|
||||
Description: "Get game by ID",
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"id": &graphql.ArgumentConfig{
|
||||
Type: graphql.NewNonNull(graphql.ID),
|
||||
},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
id, ok := p.Args["id"].(string)
|
||||
if !ok {
|
||||
log.Printf("Failed to parse ID at game: %v", p.Args["id"])
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
game, err := repo.GetGame(model.GameID(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return game, nil
|
||||
},
|
||||
},
|
||||
"gameResult": &graphql.Field{
|
||||
Type: gameResult,
|
||||
Description: "Result of a game on a players ELO",
|
||||
Args: graphql.FieldConfigArgument{
|
||||
"id": &graphql.ArgumentConfig{
|
||||
Type: graphql.NewNonNull(graphql.ID),
|
||||
},
|
||||
},
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
id, ok := p.Args["id"].(string)
|
||||
if !ok {
|
||||
log.Printf("Failed to parse ID at gameResult: %v", p.Args["id"])
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
gameResult, err := repo.GetGameResult(model.GameResultID(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gameResult, nil
|
||||
},
|
||||
},
|
||||
"players": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.NewList(player)),
|
||||
Description: "Get all players",
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
players, err := repo.GetPlayers()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return players, nil
|
||||
},
|
||||
},
|
||||
"games": &graphql.Field{
|
||||
Type: graphql.NewNonNull(graphql.NewList(game)),
|
||||
Description: "Get all games",
|
||||
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
|
||||
games, err := repo.GetGames()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return games, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
querySchema := graphql.ObjectConfig{
|
||||
Name: "Query",
|
||||
Fields: queryFields,
|
||||
}
|
||||
|
||||
schemaConfig := graphql.SchemaConfig{Query: graphql.NewObject(querySchema)}
|
||||
schema, err := graphql.NewSchema(schemaConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create new schema, error: %v", err)
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
28
internal/web/staticFiles.go
Normal file
28
internal/web/staticFiles.go
Normal file
@ -0,0 +1,28 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
//go:embed static/*
|
||||
var staticFiles embed.FS
|
||||
|
||||
type spaFileSystem struct {
|
||||
root http.FileSystem
|
||||
}
|
||||
|
||||
func (spa *spaFileSystem) Open(name string) (http.File, error) {
|
||||
f, err := spa.root.Open(name)
|
||||
if os.IsNotExist(err) {
|
||||
return spa.root.Open("index.html")
|
||||
}
|
||||
return f, err
|
||||
}
|
||||
|
||||
func initStatic(mux *http.ServeMux) {
|
||||
staticFS, _ := fs.Sub(staticFiles, "static")
|
||||
mux.Handle("/", http.FileServer(&spaFileSystem{http.FS(staticFS)}))
|
||||
}
|
24
internal/web/web.go
Normal file
24
internal/web/web.go
Normal file
@ -0,0 +1,24 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.kapelle.org/niklas/beerpong-elo/internal/repo"
|
||||
"github.com/graphql-go/handler"
|
||||
)
|
||||
|
||||
func CreateWebserver(repo repo.Repo) *http.ServeMux {
|
||||
router := http.NewServeMux()
|
||||
schema := createShema(repo)
|
||||
|
||||
gqlHandler := handler.New(&handler.Config{
|
||||
Schema: &schema,
|
||||
Pretty: true,
|
||||
GraphiQL: true,
|
||||
})
|
||||
|
||||
router.Handle("/graphql", gqlHandler)
|
||||
initStatic(router)
|
||||
|
||||
return router
|
||||
}
|
24
web/.gitignore
vendored
Normal file
24
web/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
19
web/codegen.ts
Normal file
19
web/codegen.ts
Normal file
@ -0,0 +1,19 @@
|
||||
|
||||
import type { CodegenConfig } from '@graphql-codegen/cli';
|
||||
|
||||
const config: CodegenConfig = {
|
||||
overwrite: true,
|
||||
schema: "http://localhost:8080/graphql",
|
||||
documents: "./src/**/*.svelte",
|
||||
generates: {
|
||||
"src/gql/": {
|
||||
preset: "client",
|
||||
plugins: [],
|
||||
config:{
|
||||
useTypeImports: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
13
web/index.html
Normal file
13
web/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Beerpong</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
8483
web/package-lock.json
generated
Normal file
8483
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
32
web/package.json
Normal file
32
web/package.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "beerpong-elo",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json",
|
||||
"codegen": "graphql-codegen --config codegen.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/cli": "5.0.3",
|
||||
"@graphql-codegen/client-preset": "4.5.1",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
||||
"@tsconfig/svelte": "^5.0.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"graphql": "^16.10.0",
|
||||
"graphql-request": "^7.1.2",
|
||||
"postcss": "^8.4.49",
|
||||
"svelte": "^5.15.0",
|
||||
"svelte-check": "^4.1.1",
|
||||
"svelte2tsx": "^0.7.33",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dvcol/svelte-simple-router": "^1.9.1"
|
||||
}
|
||||
}
|
6
web/postcss.config.js
Normal file
6
web/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
33
web/src/App.svelte
Normal file
33
web/src/App.svelte
Normal file
@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import Game from "./pages/Game.svelte";
|
||||
import Player from "./pages/Player.svelte";
|
||||
import { RouterView } from "@dvcol/svelte-simple-router/components";
|
||||
import type {
|
||||
Route,
|
||||
RouterOptions,
|
||||
} from "@dvcol/svelte-simple-router/models";
|
||||
|
||||
const routes: Readonly<Route[]> = [
|
||||
{
|
||||
name: "game",
|
||||
path: "/game/:{string}:id",
|
||||
component: Game,
|
||||
},
|
||||
{
|
||||
name: "player",
|
||||
path: "/player/:{string}:id",
|
||||
component: Player,
|
||||
},
|
||||
];
|
||||
|
||||
const options: RouterOptions = {
|
||||
routes,
|
||||
};
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<RouterView {options} />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
</style>
|
3
web/src/app.css
Normal file
3
web/src/app.css
Normal file
@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
67
web/src/lib/Graph.svelte
Normal file
67
web/src/lib/Graph.svelte
Normal file
@ -0,0 +1,67 @@
|
||||
<script lang="ts">
|
||||
let {
|
||||
data,
|
||||
width,
|
||||
height,
|
||||
}: { data: Array<number>; width: number; height: number } = $props();
|
||||
|
||||
const lineStep = 1;
|
||||
const labelStep = 2;
|
||||
|
||||
let minValue = $derived(Math.min(...data));
|
||||
let maxValue = $derived(Math.max(...data));
|
||||
|
||||
const xScale = (i: number) => (i / (data.length - 1)) * width;
|
||||
const yScale = (d: number) =>
|
||||
height - ((d - minValue) / (maxValue - minValue)) * height;
|
||||
|
||||
let dPath = $derived(
|
||||
data
|
||||
.map((d, i) => `${i === 0 ? "M" : "L"}${xScale(i)},${yScale(d)}`)
|
||||
.join(" "),
|
||||
);
|
||||
|
||||
let lines: Array<number> = $derived(
|
||||
Array.from(
|
||||
{ length: Math.ceil((maxValue - minValue) / lineStep) + 1 },
|
||||
(_, i) => minValue + i * lineStep,
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{width}
|
||||
{height}
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
class="border-solid border-black border-2"
|
||||
>
|
||||
<g>
|
||||
{#each lines as line}
|
||||
<line
|
||||
x1="0"
|
||||
y1={yScale(line)}
|
||||
x2={width}
|
||||
y2={yScale(line)}
|
||||
stroke="#e0e0e0"
|
||||
stroke-width="1"
|
||||
data-foo={line}
|
||||
/>
|
||||
{#if line % labelStep === 0}
|
||||
<text
|
||||
x="30"
|
||||
y={yScale(line) - 2}
|
||||
font-size="10"
|
||||
text-anchor="end"
|
||||
fill="black"
|
||||
data-foo={line}
|
||||
>
|
||||
{line}
|
||||
</text>
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<!-- Data line -->
|
||||
<path d={dPath} fill="none" stroke="orange" stroke-width="2" />
|
||||
</g>
|
||||
</svg>
|
9
web/src/main.ts
Normal file
9
web/src/main.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { mount } from 'svelte'
|
||||
import './app.css'
|
||||
import App from './App.svelte'
|
||||
|
||||
const app = mount(App, {
|
||||
target: document.getElementById('app')!,
|
||||
})
|
||||
|
||||
export default app
|
160
web/src/pages/Game.svelte
Normal file
160
web/src/pages/Game.svelte
Normal file
@ -0,0 +1,160 @@
|
||||
<script lang="ts">
|
||||
import { request } from "graphql-request";
|
||||
import { graphql } from "./../gql";
|
||||
import { onMount } from "svelte";
|
||||
import type { GetGameQuery } from "../gql/graphql";
|
||||
import { useRoute, link } from "@dvcol/svelte-simple-router/router";
|
||||
|
||||
const doc = graphql(`
|
||||
query getGame($gameID: ID!) {
|
||||
game(id: $gameID) {
|
||||
added
|
||||
overtime
|
||||
score
|
||||
author {
|
||||
ID
|
||||
name
|
||||
}
|
||||
team0player0 {
|
||||
ID
|
||||
name
|
||||
history(game: $gameID) {
|
||||
startElo
|
||||
delta
|
||||
}
|
||||
}
|
||||
team0player1 {
|
||||
ID
|
||||
name
|
||||
history(game: $gameID) {
|
||||
startElo
|
||||
delta
|
||||
}
|
||||
}
|
||||
team1player0 {
|
||||
ID
|
||||
name
|
||||
history(game: $gameID) {
|
||||
startElo
|
||||
delta
|
||||
}
|
||||
}
|
||||
team1player1 {
|
||||
ID
|
||||
name
|
||||
history(game: $gameID) {
|
||||
startElo
|
||||
delta
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
interface Player {
|
||||
name: string;
|
||||
id: string;
|
||||
startElo: number;
|
||||
delta: number;
|
||||
}
|
||||
|
||||
interface Team {
|
||||
name: string;
|
||||
p0: Player;
|
||||
p1: Player;
|
||||
}
|
||||
|
||||
let loading = $state(true);
|
||||
let errorState = $state(false);
|
||||
let data: GetGameQuery | undefined = $state();
|
||||
|
||||
onMount(() => {
|
||||
const idParam = useRoute().location?.params.id;
|
||||
if (typeof idParam == "string") {
|
||||
request(window.location.origin + "/graphql", doc, {
|
||||
gameID: idParam,
|
||||
})
|
||||
.then((e) => {
|
||||
data = e;
|
||||
loading = false;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
errorState = true;
|
||||
loading = false;
|
||||
});
|
||||
} else {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<h1 class="text-4xl">Game</h1>
|
||||
|
||||
{#if loading}
|
||||
Loading...
|
||||
{:else if errorState}
|
||||
An error occurred
|
||||
{:else if data == null || data.game == null}
|
||||
Game not found.
|
||||
{:else}
|
||||
{#snippet player(p: Player)}
|
||||
<a use:link href="/player/{p.id}"
|
||||
>{p.name} ({p.startElo} <span class="{p.delta<0?"text-red-700":"text-green-700"}" >{(p.delta<0?"":"+")+p.delta}</span>)
|
||||
</a>
|
||||
{/snippet}
|
||||
|
||||
{#snippet team(team: Team)}
|
||||
<div class="mx-3">
|
||||
<h2 class="text-xl">{team.name}</h2>
|
||||
<div>
|
||||
{@render player(team.p0)}
|
||||
</div>
|
||||
<div>
|
||||
{@render player(team.p1)}
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
Added by: {data.game.author.name} on {data.game.added}
|
||||
|
||||
<div class="flex justify-between">
|
||||
{@render team({
|
||||
name: "Team 1",
|
||||
p0: {
|
||||
id: data.game.team0player0.ID,
|
||||
name: data.game.team0player0.name,
|
||||
startElo: data.game.team0player0.history[0]!.startElo,
|
||||
delta: data.game.team0player0.history[0]!.delta,
|
||||
},
|
||||
p1: {
|
||||
id: data.game.team0player1.ID,
|
||||
name: data.game.team0player1.name,
|
||||
startElo: data.game.team0player1.history[0]!.startElo,
|
||||
delta: data.game.team0player1.history[0]!.delta,
|
||||
},
|
||||
})}
|
||||
<div class="text-2xl flex items-center">
|
||||
<div>
|
||||
<span>{data.game.score > 0 ? data.game.score : 0}</span>
|
||||
-
|
||||
<span>{data.game.score < 0 ? -data.game.score : 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
{@render team({
|
||||
name: "Team 1",
|
||||
p0: {
|
||||
id: data.game.team1player0.ID,
|
||||
name: data.game.team1player0.name,
|
||||
startElo: data.game.team1player0.history[0]!.startElo,
|
||||
delta: data.game.team1player0.history[0]!.delta,
|
||||
},
|
||||
p1: {
|
||||
id: data.game.team1player1.ID,
|
||||
name: data.game.team1player1.name,
|
||||
startElo: data.game.team1player1.history[0]!.startElo,
|
||||
delta: data.game.team1player1.history[0]!.delta,
|
||||
},
|
||||
})}
|
||||
</div>
|
||||
{/if}
|
62
web/src/pages/Player.svelte
Normal file
62
web/src/pages/Player.svelte
Normal file
@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import { request } from "graphql-request";
|
||||
import { graphql } from "./../gql";
|
||||
import { onMount } from "svelte";
|
||||
import type { GetPlayerQuery } from "../gql/graphql";
|
||||
import { useRoute } from "@dvcol/svelte-simple-router/router";
|
||||
import Graph from "../lib/Graph.svelte";
|
||||
|
||||
const doc = graphql(`
|
||||
query getPlayer($playerID: ID!) {
|
||||
player(id: $playerID) {
|
||||
ID
|
||||
name
|
||||
elo
|
||||
history {
|
||||
delta
|
||||
endElo
|
||||
game {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
let loading = $state(true);
|
||||
let errorState = $state(false);
|
||||
let data: GetPlayerQuery | undefined = $state();
|
||||
|
||||
onMount(() => {
|
||||
const idParam = useRoute().location?.params.id;
|
||||
if (typeof idParam == "string") {
|
||||
request(window.location.origin + "/graphql", doc, {
|
||||
playerID: idParam,
|
||||
})
|
||||
.then((e) => {
|
||||
data = e;
|
||||
loading = false;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
errorState = true;
|
||||
loading = false;
|
||||
});
|
||||
} else {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if loading}
|
||||
Loading...
|
||||
{:else if errorState}
|
||||
An error occurred
|
||||
{:else if data == null || data.player == null}
|
||||
Player not found.
|
||||
{:else}
|
||||
<div class="m-2">
|
||||
<h1 class="text-2xl">{data.player.name} ({data.player.elo})</h1>
|
||||
<Graph height={500} width={800} data={data.player.history.map((e) => e!.endElo)} />
|
||||
</div>
|
||||
{/if}
|
2
web/src/vite-env.d.ts
vendored
Normal file
2
web/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/// <reference types="svelte" />
|
||||
/// <reference types="vite/client" />
|
7
web/svelte.config.js
Normal file
7
web/svelte.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
export default {
|
||||
// Consult https://svelte.dev/docs#compile-time-svelte-preprocess
|
||||
// for more information about preprocessors
|
||||
preprocess: vitePreprocess(),
|
||||
}
|
12
web/tailwind.config.js
Normal file
12
web/tailwind.config.js
Normal file
@ -0,0 +1,12 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{svelte,js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
20
web/tsconfig.app.json
Normal file
20
web/tsconfig.app.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "@tsconfig/svelte/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"resolveJsonModule": true,
|
||||
/**
|
||||
* Typecheck JS in `.svelte` and `.js` files by default.
|
||||
* Disable checkJs if you'd like to use dynamic types in JS.
|
||||
* Note that setting allowJs false does not prevent the use
|
||||
* of JS in `.svelte` files.
|
||||
*/
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force"
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.svelte"]
|
||||
}
|
7
web/tsconfig.json
Normal file
7
web/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
24
web/tsconfig.node.json
Normal file
24
web/tsconfig.node.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
13
web/vite.config.ts
Normal file
13
web/vite.config.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { svelte } from '@sveltejs/vite-plugin-svelte'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
|
||||
server:{
|
||||
proxy:{
|
||||
"/graphql": "http://localhost:8080",
|
||||
},
|
||||
},
|
||||
})
|
Loading…
x
Reference in New Issue
Block a user