2
go.mod
@ -1,3 +1,5 @@
|
||||
module tilde.club/~simponic
|
||||
|
||||
go 1.21.5
|
||||
|
||||
require github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
|
2
go.sum
Normal file
@ -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=
|
126
html/fruitvote/fruits.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
BIN
html/fruitvote/fruitvote
Executable file
@ -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,
|
||||
}
|
||||
}
|
||||
|
17
html/fruitvote/templates/stats.html
Normal file
@ -0,0 +1,17 @@
|
||||
<table>
|
||||
<tr>
|
||||
<th>picture</th>
|
||||
<th>name</th>
|
||||
<th>elo</th>
|
||||
</tr>
|
||||
{{ range . }}
|
||||
<tr>
|
||||
<td><img src="{{ .Img }}" alt="{{ .Name }}" width="100" height="100"></td>
|
||||
<td>{{ .Name }}</td>
|
||||
<td>{{ .Elo }}</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</table>
|
||||
<br />
|
||||
<a href="/~simponic/fruitvote">back</a>
|
||||
<br />
|
28
html/fruitvote/templates/vote.html
Normal file
@ -0,0 +1,28 @@
|
||||
|
||||
<form method="POST">
|
||||
<div class="fruitvote">
|
||||
{{ range $i, $fruit := . }}
|
||||
{{ if $i }}
|
||||
<div class="versus">
|
||||
<h1>OR</h1>
|
||||
</div>
|
||||
{{ end }}
|
||||
<label class="contestant">
|
||||
<input type="radio" name="winner" value="{{ .Name }}" {{ if eq $i 0 }} checked {{ end }} />
|
||||
<div>
|
||||
<img src="{{ .Img }}" alt="image" />
|
||||
<p>{{ .Name }}</p>
|
||||
</div>
|
||||
</label>
|
||||
<input type="hidden" name="contestant[]" value="{{ .Name }}" />
|
||||
{{ end }}
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
<input type="submit" value="Vote" />
|
||||
</div>
|
||||
</form>
|
||||
<br />
|
||||
<div><a href="/~simponic/fruitvote"><button>Skip</button></a></div>
|
||||
<br />
|
||||
<div><a href="/~simponic/fruitvote/stats">view rankings!</a></div>
|
@ -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 */
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
7
html/public/fruitvote/health.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once("GoPage.php");
|
||||
|
||||
$page = new GoPage("/health");
|
||||
echo $page->render();
|
||||
?>
|
7
html/public/fruitvote/stats.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
require_once("GoPage.php");
|
||||
|
||||
$page = new GoPage("/stats");
|
||||
echo $page->render();
|
||||
?>
|
BIN
html/public/img/fruitvote/apricot.jpg
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
html/public/img/fruitvote/avocado.jpg
Normal file
After Width: | Height: | Size: 33 KiB |
BIN
html/public/img/fruitvote/banana.png
Normal file
After Width: | Height: | Size: 338 KiB |
BIN
html/public/img/fruitvote/blackberry.jpg
Normal file
After Width: | Height: | Size: 124 KiB |
BIN
html/public/img/fruitvote/blueberry.jpg
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
html/public/img/fruitvote/cherry.jpg
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
html/public/img/fruitvote/coconut.jpg
Normal file
After Width: | Height: | Size: 127 KiB |
BIN
html/public/img/fruitvote/cranberry.jpg
Normal file
After Width: | Height: | Size: 128 KiB |
BIN
html/public/img/fruitvote/dock-april.png
Normal file
After Width: | Height: | Size: 489 KiB |
BIN
html/public/img/fruitvote/fig.jpg
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
html/public/img/fruitvote/grape.jpg
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
html/public/img/fruitvote/guava.jpg
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
html/public/img/fruitvote/honeydew.jpeg
Normal file
After Width: | Height: | Size: 275 KiB |
BIN
html/public/img/fruitvote/kiwi.jpg
Normal file
After Width: | Height: | Size: 19 KiB |
BIN
html/public/img/fruitvote/lemon.jpg
Normal file
After Width: | Height: | Size: 127 KiB |
BIN
html/public/img/fruitvote/lime.jpg
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
html/public/img/fruitvote/mango.jpg
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
html/public/img/fruitvote/melon.jpg
Normal file
After Width: | Height: | Size: 126 KiB |
BIN
html/public/img/fruitvote/nectarine.jpg
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
html/public/img/fruitvote/orange.jpg
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
html/public/img/fruitvote/peach.jpg
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
html/public/img/fruitvote/pear.jpg
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
html/public/img/fruitvote/persimmon.jpg
Normal file
After Width: | Height: | Size: 61 KiB |
BIN
html/public/img/fruitvote/pineapple.jpg
Normal file
After Width: | Height: | Size: 64 KiB |
BIN
html/public/img/fruitvote/plum.jpg
Normal file
After Width: | Height: | Size: 59 KiB |
BIN
html/public/img/fruitvote/pomegranate.jpg
Normal file
After Width: | Height: | Size: 90 KiB |
BIN
html/public/img/fruitvote/pumpkin.jpg
Normal file
After Width: | Height: | Size: 111 KiB |
BIN
html/public/img/fruitvote/raspberry.jpg
Normal file
After Width: | Height: | Size: 171 KiB |
BIN
html/public/img/fruitvote/strawberry.jpg
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
html/public/img/fruitvote/tomato.jpg
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
html/public/img/fruitvote/versus.gif
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
html/public/img/fruitvote/watermelon.jpg
Normal file
After Width: | Height: | Size: 49 KiB |