diff --git a/go.mod b/go.mod index bb8a917..b06e58c 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module tilde.club/~simponic go 1.21.5 + +require github.com/mattn/go-sqlite3 v1.14.22 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e8d092a --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= diff --git a/html/fruitvote/fruits.json b/html/fruitvote/fruits.json new file mode 100644 index 0000000..a97f846 --- /dev/null +++ b/html/fruitvote/fruits.json @@ -0,0 +1,126 @@ +[ + { + "name": "apple", + "img": "/~simponic/img/fruitvote/dock-april.png" + }, + { + "name": "apricot", + "img": "/~simponic/img/fruitvote/apricot.jpg" + }, + { + "name": "avocado", + "img": "/~simponic/img/fruitvote/avocado.jpg" + }, + { + "name": "banana", + "img": "/~simponic/img/fruitvote/banana.png" + }, + { + "name": "blackberry", + "img": "/~simponic/img/fruitvote/blackberry.jpg" + }, + { + "name": "blueberry", + "img": "/~simponic/img/fruitvote/blueberry.jpg" + }, + { + "name": "cherry", + "img": "/~simponic/img/fruitvote/cherry.jpg" + }, + { + "name": "coconut", + "img": "/~simponic/img/fruitvote/coconut.jpg" + }, + { + "name": "cranberry", + "img": "/~simponic/img/fruitvote/cranberry.jpg" + }, + { + "name": "fig", + "img": "/~simponic/img/fruitvote/fig.jpg" + }, + { + "name": "grape", + "img": "/~simponic/img/fruitvote/grape.jpg" + }, + { + "name": "guava", + "img": "/~simponic/img/fruitvote/guava.jpg" + }, + { + "name": "honeydew", + "img": "/~simponic/img/fruitvote/honeydew.jpeg" + }, + { + "name": "kiwi", + "img": "/~simponic/img/fruitvote/kiwi.jpg" + }, + { + "name": "lemon", + "img": "/~simponic/img/fruitvote/lemon.jpg" + }, + { + "name": "lime", + "img": "/~simponic/img/fruitvote/lime.jpg" + }, + { + "name": "mango", + "img": "/~simponic/img/fruitvote/mango.jpg" + }, + { + "name": "melon", + "img": "/~simponic/img/fruitvote/melon.jpg" + }, + { + "name": "nectarine", + "img": "/~simponic/img/fruitvote/nectarine.jpg" + }, + { + "name": "orange", + "img": "/~simponic/img/fruitvote/orange.jpg" + }, + { + "name": "peach", + "img": "/~simponic/img/fruitvote/peach.jpg" + }, + { + "name": "pear", + "img": "/~simponic/img/fruitvote/pear.jpg" + }, + { + "name": "pineapple", + "img": "/~simponic/img/fruitvote/pineapple.jpg" + }, + { + "name": "persimmon", + "img": "/~simponic/img/fruitvote/persimmon.jpg" + }, + { + "name": "plum", + "img": "/~simponic/img/fruitvote/plum.jpg" + }, + { + "name": "pomegranate", + "img": "/~simponic/img/fruitvote/pomegranate.jpg" + }, + { + "name": "pumpkin", + "img": "/~simponic/img/fruitvote/pumpkin.jpg" + }, + { + "name": "raspberry", + "img": "/~simponic/img/fruitvote/raspberry.jpg" + }, + { + "name": "strawberry", + "img": "/~simponic/img/fruitvote/strawberry.jpg" + }, + { + "name": "tomato", + "img": "/~simponic/img/fruitvote/tomato.jpg" + }, + { + "name": "watermelon", + "img": "/~simponic/img/fruitvote/watermelon.jpg" + } +] diff --git a/html/fruitvote/fruitvote b/html/fruitvote/fruitvote new file mode 100755 index 0000000..51485cf Binary files /dev/null and b/html/fruitvote/fruitvote differ diff --git a/html/fruitvote/main.go b/html/fruitvote/main.go index b41c88f..cfd1bb8 100644 --- a/html/fruitvote/main.go +++ b/html/fruitvote/main.go @@ -1,9 +1,13 @@ package main import ( + "database/sql" + "encoding/json" "flag" "fmt" + _ "github.com/mattn/go-sqlite3" "log" + "math" "net" "net/http" "os" @@ -11,51 +15,241 @@ import ( "os/signal" "strings" "syscall" + "text/template" ) -func indexHandler(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("Hello, this is a Unix socket HTTP server in Go!")) +type Fruit struct { + Name string `json:"name"` + Img string `json:"img"` + Elo int `json:"elo"` } -func healthCheckHandler(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - w.Write([]byte("healthy")) +type Context struct { + db *sql.DB + users []string + templatePath string + socketPath string + fruitsPath string } -func main() { - socketPath, users := getArgs() - os.Remove(socketPath) +type CurriedContextHandler func(*Context, http.ResponseWriter, *http.Request) - listener, err := net.Listen("unix", socketPath) +func curryContext(context *Context) func(CurriedContextHandler) http.HandlerFunc { + return func(handler CurriedContextHandler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + log.Println(r.Method, r.URL.Path, r.RemoteAddr) + + handler(context, w, r) + } + } +} + +func indexHandler(context *Context, resp http.ResponseWriter, req *http.Request) { + // get from POST + winner := req.FormValue("winner") + contestants := req.Form["contestant[]"] + + if winner != "" && len(contestants) == 2 { + losingFruitName := string(contestants[0]) + if losingFruitName == winner { + losingFruitName = string(contestants[1]) + } + winningFruit := fruitByName(context.db, winner) + losingFruit := fruitByName(context.db, losingFruitName) + + winningFruit.Elo, losingFruit.Elo = updateElo(winningFruit.Elo, losingFruit.Elo) + + updateFruit(context.db, winningFruit) + updateFruit(context.db, losingFruit) + + log.Println(winningFruit.Name, "won against", losingFruit.Name, "new elo:", winningFruit.Elo, losingFruit.Elo) + } + + fruitOne := randomFruit(context.db) + fruitTwo := randomFruit(context.db) + for fruitOne.Name == fruitTwo.Name { + fruitTwo = randomFruit(context.db) + } + + templateFile := context.templatePath + "/vote.html" + vote, err := template.ParseFiles(templateFile) if err != nil { panic(err) } - os.Chmod(socketPath, 0700) + + fruits := []Fruit{fruitOne, fruitTwo} + err = vote.Execute(resp, fruits) + if err != nil { + panic(err) + } + resp.Header().Set("Content-Type", "text/html") +} + +func getStatsHandler(context *Context, resp http.ResponseWriter, _req *http.Request) { + rows, err := context.db.Query("SELECT name, img, elo FROM fruits ORDER BY elo DESC") + if err != nil { + panic(err) + } + defer rows.Close() + + fruits := []Fruit{} + for rows.Next() { + fruit := Fruit{} + err = rows.Scan(&fruit.Name, &fruit.Img, &fruit.Elo) + if err != nil { + panic(err) + } + fruits = append(fruits, fruit) + } + + templateFile := context.templatePath + "/stats.html" + stats, err := template.ParseFiles(templateFile) + if err != nil { + panic(err) + } + + err = stats.Execute(resp, fruits) + if err != nil { + panic(err) + } + resp.Header().Set("Content-Type", "text/html") +} + +func healthCheckHandler(context *Context, resp http.ResponseWriter, req *http.Request) { + resp.WriteHeader(http.StatusOK) + resp.Write([]byte("healthy")) +} + +func main() { + log.Println("starting server...") + log.SetFlags(log.LstdFlags | log.Lshortfile) + + context := getArgs() + log.Println("removing socket file", context.socketPath) + os.Remove(context.socketPath) + + log.Println("migrating database...") + migrate(context.db) + seedFruits(context.db, context.fruitsPath) + + listener, err := net.Listen("unix", context.socketPath) + if err != nil { + panic(err) + } + log.Println("listening on", context.socketPath) + os.Chmod(context.socketPath, 0700) sigc := make(chan os.Signal, 1) signal.Notify(sigc, os.Interrupt, os.Kill, syscall.SIGTERM) go func(c chan os.Signal) { - // Wait for a SIGINT or SIGKILL: sig := <-c - log.Printf("Caught signal %s: shutting down.", sig) + log.Printf("caught signal %s: shutting down.", sig) listener.Close() os.Exit(0) }(sigc) defer listener.Close() - for _, user := range strings.Split(users, ",") { - setACL(socketPath, user) + log.Println("setting ACLs for users", context.users) + for _, user := range context.users { + setACL(context.socketPath, user) } + curriedContext := curryContext(context) mux := http.NewServeMux() - mux.HandleFunc("/", indexHandler) - mux.HandleFunc("/health", healthCheckHandler) + mux.HandleFunc("/", curriedContext(indexHandler)) + mux.HandleFunc("/health", curriedContext(healthCheckHandler)) + mux.HandleFunc("/stats", curriedContext(getStatsHandler)) + log.Println("serving http...") http.Serve(listener, mux) } +func calculateRatingDelta(winnerRating int, loserRating int, K int) (int, int) { + winnerRatingFloat := float64(winnerRating) + loserRatingFloat := float64(loserRating) + + expectedScoreWinner := 1 / (1 + math.Pow(10, (loserRatingFloat-winnerRatingFloat)/400)) + expectedScoreLoser := 1 - expectedScoreWinner + + changeWinner := int(math.Round(float64(K) * (1 - expectedScoreWinner))) + changeLoser := -int(math.Round(float64(K) * (expectedScoreLoser))) + + return changeWinner, changeLoser +} + +func updateElo(winnerElo int, loserElo int) (int, int) { + const K = 32 + + changeWinner, changeLoser := calculateRatingDelta(winnerElo, loserElo, K) + + newWinnerElo := winnerElo + changeWinner + newLoserElo := loserElo + changeLoser + + return newWinnerElo, newLoserElo +} + +func seedFruits(db *sql.DB, fruitsPath string) { + log.Println("seeding fruits (haha)...") + + fruitsContents, err := os.ReadFile(fruitsPath) + if err != nil { + panic(err) + } + jsonFruits := []Fruit{} + err = json.Unmarshal(fruitsContents, &jsonFruits) + if err != nil { + panic(err) + } + + for _, fruit := range jsonFruits { + // insert if not exists + _, err := db.Exec("INSERT OR IGNORE INTO fruits (name, img) VALUES (?, ?)", fruit.Name, fruit.Img) + if err != nil { + panic(err) + } + } +} + +func migrate(db *sql.DB) { + log.Println("creating fruits table...") + _, err := db.Exec(` + CREATE TABLE IF NOT EXISTS fruits ( + name TEXT PRIMARY KEY, + img TEXT, + elo INTEGER DEFAULT 1400 + ); + `) + if err != nil { + panic(err) + } +} + +func randomFruit(db *sql.DB) Fruit { + fruit := Fruit{} + err := db.QueryRow("SELECT name, img, elo FROM fruits ORDER BY RANDOM() LIMIT 1").Scan(&fruit.Name, &fruit.Img, &fruit.Elo) + if err != nil { + panic(err) + } + return fruit +} + +func fruitByName(db *sql.DB, name string) Fruit { + fruit := Fruit{} + err := db.QueryRow("SELECT name, img, elo FROM fruits WHERE name = ?", name).Scan(&fruit.Name, &fruit.Img, &fruit.Elo) + if err != nil { + panic(err) + } + return fruit +} + +func updateFruit(db *sql.DB, fruit Fruit) { + _, err := db.Exec("UPDATE fruits SET img = ?, elo = ? WHERE name = ?", fruit.Img, fruit.Elo, fruit.Name) + if err != nil { + panic(err) + } +} + func setACL(socketPath, user string) { cmd := exec.Command("setfacl", "-m", "u:"+user+":rwx", socketPath) if err := cmd.Run(); err != nil { @@ -63,15 +257,34 @@ func setACL(socketPath, user string) { } } -func getArgs() (string, string) { +func getArgs() *Context { socketPath := flag.String("socket-path", "/tmp/go-server.sock", "Path to the Unix socket") users := flag.String("users", "", "Comma-separated list of users for ACL") + database := flag.String("database-path", "/tmp/go-server.db", "Path to the SQLite database") + fruitsPath := flag.String("fruits", "/dev/null", "Path to the fruits file") + templatePath := flag.String("template", "", "Path to the template directory") flag.Parse() if *users == "" { fmt.Println("You must specify at least one user with --users") os.Exit(1) } + if *templatePath == "" { + fmt.Println("You must specify a template directory with --template") + os.Exit(1) + } - return *socketPath, *users + log.Println("opening database at", *database, "with foreign keys enabled") + db, err := sql.Open("sqlite3", *database+"?_foreign_keys=on") + if err != nil { + panic(err) + } + + return &Context{ + db: db, + users: strings.Split(*users, ","), + socketPath: *socketPath, + fruitsPath: *fruitsPath, + templatePath: *templatePath, + } } diff --git a/html/fruitvote/templates/stats.html b/html/fruitvote/templates/stats.html new file mode 100644 index 0000000..c5fea5c --- /dev/null +++ b/html/fruitvote/templates/stats.html @@ -0,0 +1,17 @@ + + + + + + + {{ range . }} + + + + + + {{ end }} +
picturenameelo
{{ .Name }}{{ .Name }}{{ .Elo }}
+
+back +
diff --git a/html/fruitvote/templates/vote.html b/html/fruitvote/templates/vote.html new file mode 100644 index 0000000..6bc27df --- /dev/null +++ b/html/fruitvote/templates/vote.html @@ -0,0 +1,28 @@ + +
+
+ {{ range $i, $fruit := . }} + {{ if $i }} +
+

OR

+
+ {{ end }} + + + {{ end }} +
+
+
+ +
+
+
+
+
+
view rankings!
diff --git a/html/public/css/style.css b/html/public/css/style.css index a4f243f..95829c3 100644 --- a/html/public/css/style.css +++ b/html/public/css/style.css @@ -75,3 +75,100 @@ p { li { margin-left: 20px; } + +.fruitvote { + display: flex; + flex-direction: row; + margin-top: 20px; + gap: 2rem; + max-width: 800px; +} + +.contestant { + display: flex; + flex: 1; + flex-direction: column; + align-items: stretch; + border: 2px solid #ff69b4; + border-radius: 10px; + padding: 0.5rem; +} + +.contestant div { + display: flex; + flex-direction: column; + justify-content: space-between; + border-radius: 10px; + height: 100%; + transition: background-color 0.3s ease; + padding: 1rem; +} + +.contestant > input { + visibility: hidden; + position: absolute; +} + +.contestant div:hover { + background-color: #ff69b4; + color: #2a2a2a; + + cursor: pointer; +} + +.contestant > input:checked + div { + background-color: #ff69b4; + color: #2a2a2a; + + cursor: pointer; +} + +.contestant div img { + width: auto; + height: auto; + max-width: 100%; + max-height: 100%; + + border-radius: 10px; +} + +.versus { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; +} + +table { + width: auto; /* Adjust based on content, not full width */ + border-collapse: collapse; + background-color: #383838; /* Darker background for contrast */ +} + +th, +td { + padding: 12px 20px; /* Good padding for readability */ + border: 1px solid #f4c2c2; /* Soft pink borders */ + color: #f4c2c2; /* Soft pink text */ + text-align: left; +} + +thead th { + background-color: #ff69b4; /* Brighter pink for header */ + color: white; /* White text for contrast */ + font-family: "Comic Sans MS", "Chalkboard SE", sans-serif; +} + +tbody tr:nth-child(odd) { + background-color: #2f2f2f; /* Slightly lighter background for every other row for readability */ +} + +tbody tr { + transition: background-color 0.3s ease; +} + +tbody tr:hover { + background-color: #ff47da; /* Change to a lighter pink on hover for interactivity */ + color: #2a2a2a; /* Dark text for contrast */ +} diff --git a/html/public/fruitvote/GoPage.php b/html/public/fruitvote/GoPage.php index 864c1f7..7e03c35 100644 --- a/html/public/fruitvote/GoPage.php +++ b/html/public/fruitvote/GoPage.php @@ -4,7 +4,7 @@ class GoPage { private $socket; private $template; - public function __construct($page, $socket = "/home/simponic/fruitvote/http.sock", $template = "../template.html") { + public function __construct($page, $socket = "/home/lizzy/fruitvote/http.sock", $template = "../template.html") { $this->page = $page; $this->socket = $socket; $this->template = $template; @@ -24,9 +24,23 @@ class GoPage { curl_setopt($ch, CURLOPT_UNIX_SOCKET_PATH, $this->socket); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + // forward query params + $query = $_SERVER['QUERY_STRING']; + if ($query) { + curl_setopt($ch, CURLOPT_URL, $url."?".$query); + } + + //forward post data + if ($_SERVER['REQUEST_METHOD'] === 'POST') { + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, file_get_contents('php://input')); + } + + $output = curl_exec($ch); curl_close($ch); - // todo: get headers / cookies, forward back response + + return $output; } diff --git a/html/public/fruitvote/health.php b/html/public/fruitvote/health.php new file mode 100644 index 0000000..8cea676 --- /dev/null +++ b/html/public/fruitvote/health.php @@ -0,0 +1,7 @@ +render(); +?> diff --git a/html/public/fruitvote/stats.php b/html/public/fruitvote/stats.php new file mode 100644 index 0000000..5147723 --- /dev/null +++ b/html/public/fruitvote/stats.php @@ -0,0 +1,7 @@ +render(); +?> diff --git a/html/public/img/fruitvote/apricot.jpg b/html/public/img/fruitvote/apricot.jpg new file mode 100644 index 0000000..5ffc321 Binary files /dev/null and b/html/public/img/fruitvote/apricot.jpg differ diff --git a/html/public/img/fruitvote/avocado.jpg b/html/public/img/fruitvote/avocado.jpg new file mode 100644 index 0000000..a0eb21c Binary files /dev/null and b/html/public/img/fruitvote/avocado.jpg differ diff --git a/html/public/img/fruitvote/banana.png b/html/public/img/fruitvote/banana.png new file mode 100644 index 0000000..6e69d77 Binary files /dev/null and b/html/public/img/fruitvote/banana.png differ diff --git a/html/public/img/fruitvote/blackberry.jpg b/html/public/img/fruitvote/blackberry.jpg new file mode 100644 index 0000000..e097829 Binary files /dev/null and b/html/public/img/fruitvote/blackberry.jpg differ diff --git a/html/public/img/fruitvote/blueberry.jpg b/html/public/img/fruitvote/blueberry.jpg new file mode 100644 index 0000000..9794493 Binary files /dev/null and b/html/public/img/fruitvote/blueberry.jpg differ diff --git a/html/public/img/fruitvote/cherry.jpg b/html/public/img/fruitvote/cherry.jpg new file mode 100644 index 0000000..277c17c Binary files /dev/null and b/html/public/img/fruitvote/cherry.jpg differ diff --git a/html/public/img/fruitvote/coconut.jpg b/html/public/img/fruitvote/coconut.jpg new file mode 100644 index 0000000..76044f4 Binary files /dev/null and b/html/public/img/fruitvote/coconut.jpg differ diff --git a/html/public/img/fruitvote/cranberry.jpg b/html/public/img/fruitvote/cranberry.jpg new file mode 100644 index 0000000..fe8513a Binary files /dev/null and b/html/public/img/fruitvote/cranberry.jpg differ diff --git a/html/public/img/fruitvote/dock-april.png b/html/public/img/fruitvote/dock-april.png new file mode 100644 index 0000000..1cd82d2 Binary files /dev/null and b/html/public/img/fruitvote/dock-april.png differ diff --git a/html/public/img/fruitvote/fig.jpg b/html/public/img/fruitvote/fig.jpg new file mode 100644 index 0000000..643a49b Binary files /dev/null and b/html/public/img/fruitvote/fig.jpg differ diff --git a/html/public/img/fruitvote/grape.jpg b/html/public/img/fruitvote/grape.jpg new file mode 100644 index 0000000..ce341d4 Binary files /dev/null and b/html/public/img/fruitvote/grape.jpg differ diff --git a/html/public/img/fruitvote/guava.jpg b/html/public/img/fruitvote/guava.jpg new file mode 100644 index 0000000..953ed0b Binary files /dev/null and b/html/public/img/fruitvote/guava.jpg differ diff --git a/html/public/img/fruitvote/honeydew.jpeg b/html/public/img/fruitvote/honeydew.jpeg new file mode 100644 index 0000000..1e796e5 Binary files /dev/null and b/html/public/img/fruitvote/honeydew.jpeg differ diff --git a/html/public/img/fruitvote/kiwi.jpg b/html/public/img/fruitvote/kiwi.jpg new file mode 100644 index 0000000..6b048d5 Binary files /dev/null and b/html/public/img/fruitvote/kiwi.jpg differ diff --git a/html/public/img/fruitvote/lemon.jpg b/html/public/img/fruitvote/lemon.jpg new file mode 100644 index 0000000..9623514 Binary files /dev/null and b/html/public/img/fruitvote/lemon.jpg differ diff --git a/html/public/img/fruitvote/lime.jpg b/html/public/img/fruitvote/lime.jpg new file mode 100644 index 0000000..10302fa Binary files /dev/null and b/html/public/img/fruitvote/lime.jpg differ diff --git a/html/public/img/fruitvote/mango.jpg b/html/public/img/fruitvote/mango.jpg new file mode 100644 index 0000000..a9eb35e Binary files /dev/null and b/html/public/img/fruitvote/mango.jpg differ diff --git a/html/public/img/fruitvote/melon.jpg b/html/public/img/fruitvote/melon.jpg new file mode 100644 index 0000000..dc45f27 Binary files /dev/null and b/html/public/img/fruitvote/melon.jpg differ diff --git a/html/public/img/fruitvote/nectarine.jpg b/html/public/img/fruitvote/nectarine.jpg new file mode 100644 index 0000000..24fddbe Binary files /dev/null and b/html/public/img/fruitvote/nectarine.jpg differ diff --git a/html/public/img/fruitvote/orange.jpg b/html/public/img/fruitvote/orange.jpg new file mode 100644 index 0000000..1bb12a1 Binary files /dev/null and b/html/public/img/fruitvote/orange.jpg differ diff --git a/html/public/img/fruitvote/peach.jpg b/html/public/img/fruitvote/peach.jpg new file mode 100644 index 0000000..9690ca6 Binary files /dev/null and b/html/public/img/fruitvote/peach.jpg differ diff --git a/html/public/img/fruitvote/pear.jpg b/html/public/img/fruitvote/pear.jpg new file mode 100644 index 0000000..22c2a01 Binary files /dev/null and b/html/public/img/fruitvote/pear.jpg differ diff --git a/html/public/img/fruitvote/persimmon.jpg b/html/public/img/fruitvote/persimmon.jpg new file mode 100644 index 0000000..6af3afe Binary files /dev/null and b/html/public/img/fruitvote/persimmon.jpg differ diff --git a/html/public/img/fruitvote/pineapple.jpg b/html/public/img/fruitvote/pineapple.jpg new file mode 100644 index 0000000..bbeeeb2 Binary files /dev/null and b/html/public/img/fruitvote/pineapple.jpg differ diff --git a/html/public/img/fruitvote/plum.jpg b/html/public/img/fruitvote/plum.jpg new file mode 100644 index 0000000..61a8893 Binary files /dev/null and b/html/public/img/fruitvote/plum.jpg differ diff --git a/html/public/img/fruitvote/pomegranate.jpg b/html/public/img/fruitvote/pomegranate.jpg new file mode 100644 index 0000000..5be6859 Binary files /dev/null and b/html/public/img/fruitvote/pomegranate.jpg differ diff --git a/html/public/img/fruitvote/pumpkin.jpg b/html/public/img/fruitvote/pumpkin.jpg new file mode 100644 index 0000000..8a20485 Binary files /dev/null and b/html/public/img/fruitvote/pumpkin.jpg differ diff --git a/html/public/img/fruitvote/raspberry.jpg b/html/public/img/fruitvote/raspberry.jpg new file mode 100644 index 0000000..07a33b2 Binary files /dev/null and b/html/public/img/fruitvote/raspberry.jpg differ diff --git a/html/public/img/fruitvote/strawberry.jpg b/html/public/img/fruitvote/strawberry.jpg new file mode 100644 index 0000000..22ee16b Binary files /dev/null and b/html/public/img/fruitvote/strawberry.jpg differ diff --git a/html/public/img/fruitvote/tomato.jpg b/html/public/img/fruitvote/tomato.jpg new file mode 100644 index 0000000..846fb08 Binary files /dev/null and b/html/public/img/fruitvote/tomato.jpg differ diff --git a/html/public/img/fruitvote/versus.gif b/html/public/img/fruitvote/versus.gif new file mode 100644 index 0000000..6d45ee6 Binary files /dev/null and b/html/public/img/fruitvote/versus.gif differ diff --git a/html/public/img/fruitvote/watermelon.jpg b/html/public/img/fruitvote/watermelon.jpg new file mode 100644 index 0000000..1db28d9 Binary files /dev/null and b/html/public/img/fruitvote/watermelon.jpg differ