From 5926bd49e85e10ab594b6949d20f9cb7c5ff6443 Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Mon, 23 Jul 2018 23:13:29 +0200 Subject: [PATCH 01/20] Add skeleton of WebUI: list, backup, restore --- webui.go | 108 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 webui.go diff --git a/webui.go b/webui.go new file mode 100644 index 0000000..f8f037a --- /dev/null +++ b/webui.go @@ -0,0 +1,108 @@ +// See LICENSE.txt for licensing information. + +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "strconv" + "time" +) + +import ( + "github.com/gorilla/mux" +) + +var webuiLog *log.Logger + +func handleGetList(w http.ResponseWriter, r *http.Request) { + j := json.NewEncoder(w) + w.Header()["Content-Type"] = []string{"application/json"} + j.Encode(cfg) +} + +func handleGetGameBackup(w http.ResponseWriter, r *http.Request) { + g, ok := getGame(w, r) + if !ok { + return + } + if err := r.ParseForm(); err != nil { + webuiLog.Println("FAILED to parse form") + http.Error(w, "Couldn't parse form.", 400) + return + } + note := r.Form.Get("note") + + sv, err := g.Backup() + if err != nil { + webuiLog.Println("FAILED to save: ", err) + http.Error(w, fmt.Sprintln("Failed to save: ", err), 501) + return + } + if note != "" { + sv.Note = note + } + + outputJSON(w, sv) +} + +func handleGetGameRestore(w http.ResponseWriter, r *http.Request) { + g, ok := getGame(w, r) + if !ok { + return + } + v := mux.Vars(r) + i, _ := strconv.Atoi(v["id"]) + sv, err := g.Restore(i) + if err != nil { + webuiLog.Println("FAILED to restore: ", err) + http.Error(w, fmt.Sprintln("Failed to restore: ", err), 501) + return + } + g.Stamp = time.Now() + + outputJSON(w, sv) +} + +func loggingMw(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + webuiLog.Printf("Request to %s\n", r.RequestURI) + next.ServeHTTP(w, r) + }) +} + +func startWebUI() { + webuiLog = log.New(os.Stdout, "", log.Ltime|log.Lshortfile) + + r := mux.NewRouter() + r.HandleFunc("/list", handleGetList).Methods("GET") + r.HandleFunc("/{game}/backup", handleGetGameBackup).Methods("GET") + r.HandleFunc("/{game}/restore/{id:[0-9]+}", handleGetGameRestore).Methods("GET") + r.Use(loggingMw) + http.Handle("/", r) + + addr := fmt.Sprintf("127.0.0.1:%d", flagPort) + webuiLog.Printf("Starting Web UI at http://%s\n", addr) + http.ListenAndServe(addr, nil) +} + +func getGame(w http.ResponseWriter, r *http.Request) (*Game, bool) { + v := mux.Vars(r) + game := v["game"] + g := cfg.GetGame(game) + if g == nil { + webuiLog.Printf("Game '%s' not found\n", game) + http.NotFound(w, r) + return nil, false + } + return g, true +} + +func outputJSON(w http.ResponseWriter, data interface{}) { + j := json.NewEncoder(w) + w.Header()["Content-Type"] = []string{"application/json"} + j.Encode(data) +} From 9fdc1953634964ae623a64abcecdec177041a233 Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Mon, 23 Jul 2018 23:13:46 +0200 Subject: [PATCH 02/20] Add signal handlers --- sigwait_unix.go | 23 +++++++++++++++++++++++ sigwait_windows.go | 22 ++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 sigwait_unix.go create mode 100644 sigwait_windows.go diff --git a/sigwait_unix.go b/sigwait_unix.go new file mode 100644 index 0000000..3973d2e --- /dev/null +++ b/sigwait_unix.go @@ -0,0 +1,23 @@ +// See LICENSE.txt for licensing information. +// +build !windows + +package main + +import ( + "fmt" + "os" + "os/signal" + "syscall" +) + +// sigwait processes signals such as a CTRL-C hit. +func sigwait() { + sig := make(chan os.Signal) + signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL) + s := <-sig + if s == syscall.SIGINT { + fmt.Println() + } + webuiLog.Printf("Signal '%s' received, stopping", s) + return +} diff --git a/sigwait_windows.go b/sigwait_windows.go new file mode 100644 index 0000000..cb1c5b7 --- /dev/null +++ b/sigwait_windows.go @@ -0,0 +1,22 @@ +// See LICENSE.txt for licensing information. +// +build windows + +package main + +import ( + "fmt" + "os" + "os/signal" +) + +// sigwait processes signals such as a CTRL-C hit. +func sigwait() { + sig := make(chan os.Signal) + signal.Notify(sig, os.Interrupt, os.Kill) + s := <-sig + if s == os.Interrupt { + fmt.Println() + } + webuiLog.Printf("Signal '%s' received, stopping", s) + return +} From 4decca7e6ca0e71c3ae7c3e664a220dd74ae634c Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Mon, 23 Jul 2018 23:14:11 +0200 Subject: [PATCH 03/20] Plug in the webui into main --- main.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/main.go b/main.go index fe2e88d..1b6c6f4 100644 --- a/main.go +++ b/main.go @@ -23,6 +23,7 @@ var build = `UNKNOWN` // injected in Makefile var ( flagConfig string flagVerbose bool + flagPort int cfg *Config idRange = regexp.MustCompile(`(\d+)-(\d+)`) spinner = Spinner{} @@ -47,6 +48,7 @@ Commands: [del]ete - delete given save(s) [kill] - delete game and all saves [migrate] - migrate config, if needed + [webui] - start the Web UI Where: name - arbitrary name used to identify a game/character/world etc. @@ -58,6 +60,7 @@ Where: } flag.StringVar(&flagConfig, "c", "saver.json", "path to config file") flag.BoolVar(&flagVerbose, "v", false, "be very verbose") + flag.IntVar(&flagPort, "p", 8888, "Web UI port") } func main() { @@ -96,6 +99,11 @@ func main() { err := cfg.Migrate() dieOnErr("ERROR", err) save = true + case "webui": + // start the Web UI + go startWebUI() + sigwait() + save = true default: // per-game commands checkArgs(false, 2) From 2e67128fa8f94d119011741277facd44920d12ab Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Mon, 23 Jul 2018 23:14:38 +0200 Subject: [PATCH 04/20] Change version to indicate we're doing stuff here --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 1b6c6f4..d820d9f 100644 --- a/main.go +++ b/main.go @@ -13,7 +13,7 @@ import ( ) const ( - VERSION = `0.9.1` + VERSION = `0.9.1-webui` timeFmt = `2006-01-02 15:04:05` fileFmt = `2006-01-02_150405` ) From 2366f46bf2ce1cd7764832bf8e943906826b1514 Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Mon, 23 Jul 2018 23:21:27 +0200 Subject: [PATCH 05/20] Use the new helper in list endpoint --- webui.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/webui.go b/webui.go index f8f037a..90f55f6 100644 --- a/webui.go +++ b/webui.go @@ -19,9 +19,7 @@ import ( var webuiLog *log.Logger func handleGetList(w http.ResponseWriter, r *http.Request) { - j := json.NewEncoder(w) - w.Header()["Content-Type"] = []string{"application/json"} - j.Encode(cfg) + outputJSON(w, cfg) } func handleGetGameBackup(w http.ResponseWriter, r *http.Request) { From 1db5859daaf5fca409a5163831c186f6690b3e73 Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Mon, 23 Jul 2018 23:33:17 +0200 Subject: [PATCH 06/20] Extract and reuse common signal logic Still not sure what to do about darwin builds... --- sigwait_common.go | 20 ++++++++++++++++++++ sigwait_unix.go | 16 ++-------------- sigwait_windows.go | 15 ++------------- 3 files changed, 24 insertions(+), 27 deletions(-) create mode 100644 sigwait_common.go diff --git a/sigwait_common.go b/sigwait_common.go new file mode 100644 index 0000000..7ec84a5 --- /dev/null +++ b/sigwait_common.go @@ -0,0 +1,20 @@ +// See LICENSE.txt for licensing information. + +package main + +import ( + "fmt" + "os" + "os/signal" +) + +func _sigwait(sigs ...os.Signal) { + sig := make(chan os.Signal) + signal.Notify(sig, sigs...) + s := <-sig + if s == sigs[0] { + fmt.Println() + } + webuiLog.Printf("Signal '%s' received, stopping", s) + return +} diff --git a/sigwait_unix.go b/sigwait_unix.go index 3973d2e..3ea6e69 100644 --- a/sigwait_unix.go +++ b/sigwait_unix.go @@ -3,21 +3,9 @@ package main -import ( - "fmt" - "os" - "os/signal" - "syscall" -) +import "syscall" // sigwait processes signals such as a CTRL-C hit. func sigwait() { - sig := make(chan os.Signal) - signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL) - s := <-sig - if s == syscall.SIGINT { - fmt.Println() - } - webuiLog.Printf("Signal '%s' received, stopping", s) - return + _sigwait(syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL) } diff --git a/sigwait_windows.go b/sigwait_windows.go index cb1c5b7..55d747d 100644 --- a/sigwait_windows.go +++ b/sigwait_windows.go @@ -3,20 +3,9 @@ package main -import ( - "fmt" - "os" - "os/signal" -) +import "os" // sigwait processes signals such as a CTRL-C hit. func sigwait() { - sig := make(chan os.Signal) - signal.Notify(sig, os.Interrupt, os.Kill) - s := <-sig - if s == os.Interrupt { - fmt.Println() - } - webuiLog.Printf("Signal '%s' received, stopping", s) - return + _sigwait(os.Interrupt, os.Kill) } From a7dd41227e1bf043540bb91d85342e2afa0d513b Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Tue, 24 Jul 2018 18:48:37 +0200 Subject: [PATCH 07/20] Simplify save note handling on backup --- webui.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/webui.go b/webui.go index 90f55f6..24e8ff6 100644 --- a/webui.go +++ b/webui.go @@ -32,17 +32,13 @@ func handleGetGameBackup(w http.ResponseWriter, r *http.Request) { http.Error(w, "Couldn't parse form.", 400) return } - note := r.Form.Get("note") - sv, err := g.Backup() if err != nil { webuiLog.Println("FAILED to save: ", err) http.Error(w, fmt.Sprintln("Failed to save: ", err), 501) return } - if note != "" { - sv.Note = note - } + sv.Note = r.Form.Get("note") outputJSON(w, sv) } From 74d1e1712d5bb5d73b5442e5d109f106cbd3669e Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Tue, 24 Jul 2018 19:03:02 +0200 Subject: [PATCH 08/20] Add delete endpoint --- webui.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/webui.go b/webui.go index 24e8ff6..9a29bbb 100644 --- a/webui.go +++ b/webui.go @@ -61,6 +61,23 @@ func handleGetGameRestore(w http.ResponseWriter, r *http.Request) { outputJSON(w, sv) } +func handleGetGameDelete(w http.ResponseWriter, r *http.Request) { + g, ok := getGame(w, r) + if !ok { + return + } + v := mux.Vars(r) + f, _ := strconv.Atoi(v["from"]) + t, _ := strconv.Atoi(v["to"]) + _, err := g.Delete(f, t) + if err != nil { + webuiLog.Println("FAILED to delete: ", err) + http.Error(w, fmt.Sprintln("Failed to delete: ", err), 501) + } + + outputJSON(w, cfg) +} + func loggingMw(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { webuiLog.Printf("Request to %s\n", r.RequestURI) @@ -75,6 +92,7 @@ func startWebUI() { r.HandleFunc("/list", handleGetList).Methods("GET") r.HandleFunc("/{game}/backup", handleGetGameBackup).Methods("GET") r.HandleFunc("/{game}/restore/{id:[0-9]+}", handleGetGameRestore).Methods("GET") + r.HandleFunc("/{game}/delete/{from:[0-9]+}-{to:[0-9]+}", handleGetGameDelete).Methods("GET") r.Use(loggingMw) http.Handle("/", r) From f70bec22434b4df17ca92369f5b01e389d7e637d Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Tue, 24 Jul 2018 19:04:29 +0200 Subject: [PATCH 09/20] Use consts and reformat router config --- webui.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/webui.go b/webui.go index 9a29bbb..66f2eb4 100644 --- a/webui.go +++ b/webui.go @@ -89,10 +89,14 @@ func startWebUI() { webuiLog = log.New(os.Stdout, "", log.Ltime|log.Lshortfile) r := mux.NewRouter() - r.HandleFunc("/list", handleGetList).Methods("GET") - r.HandleFunc("/{game}/backup", handleGetGameBackup).Methods("GET") - r.HandleFunc("/{game}/restore/{id:[0-9]+}", handleGetGameRestore).Methods("GET") - r.HandleFunc("/{game}/delete/{from:[0-9]+}-{to:[0-9]+}", handleGetGameDelete).Methods("GET") + r.HandleFunc("/list", handleGetList). + Methods(http.MethodGet) + r.HandleFunc("/{game}/backup", handleGetGameBackup). + Methods(http.MethodGet) + r.HandleFunc("/{game}/restore/{id:[0-9]+}", handleGetGameRestore). + Methods(http.MethodGet) + r.HandleFunc("/{game}/delete/{from:[0-9]+}-{to:[0-9]+}", handleGetGameDelete). + Methods(http.MethodGet) r.Use(loggingMw) http.Handle("/", r) From 8f98a73f4f74aa63d33196600c549d24c3275497 Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Tue, 24 Jul 2018 19:18:18 +0200 Subject: [PATCH 10/20] Fix and move status codes to consts too --- webui.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/webui.go b/webui.go index 66f2eb4..1917053 100644 --- a/webui.go +++ b/webui.go @@ -29,13 +29,13 @@ func handleGetGameBackup(w http.ResponseWriter, r *http.Request) { } if err := r.ParseForm(); err != nil { webuiLog.Println("FAILED to parse form") - http.Error(w, "Couldn't parse form.", 400) + http.Error(w, "Couldn't parse form.", http.StatusBadRequest) return } sv, err := g.Backup() if err != nil { webuiLog.Println("FAILED to save: ", err) - http.Error(w, fmt.Sprintln("Failed to save: ", err), 501) + http.Error(w, fmt.Sprintln("Failed to save: ", err), http.StatusInternalServerError) return } sv.Note = r.Form.Get("note") @@ -53,7 +53,7 @@ func handleGetGameRestore(w http.ResponseWriter, r *http.Request) { sv, err := g.Restore(i) if err != nil { webuiLog.Println("FAILED to restore: ", err) - http.Error(w, fmt.Sprintln("Failed to restore: ", err), 501) + http.Error(w, fmt.Sprintln("Failed to restore: ", err), http.StatusInternalServerError) return } g.Stamp = time.Now() @@ -72,7 +72,7 @@ func handleGetGameDelete(w http.ResponseWriter, r *http.Request) { _, err := g.Delete(f, t) if err != nil { webuiLog.Println("FAILED to delete: ", err) - http.Error(w, fmt.Sprintln("Failed to delete: ", err), 501) + http.Error(w, fmt.Sprintln("Failed to delete: ", err), http.StatusInternalServerError) } outputJSON(w, cfg) From 533de1cb4025d793338ad53ac2e116a7077bb058 Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Tue, 24 Jul 2018 20:13:59 +0200 Subject: [PATCH 11/20] FEnd effort started --- js/.gitignore | 2 ++ js/package.json | 41 ++++++++++++++++++++++++++++++++++ js/src/index.coffee | 12 ++++++++++ js/src/index.hbs | 15 +++++++++++++ js/src/index.less | 4 ++++ js/webpack.common.js | 53 ++++++++++++++++++++++++++++++++++++++++++++ js/webpack.dev.js | 9 ++++++++ js/webpack.prod.js | 10 +++++++++ 8 files changed, 146 insertions(+) create mode 100644 js/.gitignore create mode 100644 js/package.json create mode 100644 js/src/index.coffee create mode 100644 js/src/index.hbs create mode 100644 js/src/index.less create mode 100644 js/webpack.common.js create mode 100644 js/webpack.dev.js create mode 100644 js/webpack.prod.js diff --git a/js/.gitignore b/js/.gitignore new file mode 100644 index 0000000..b947077 --- /dev/null +++ b/js/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +dist/ diff --git a/js/package.json b/js/package.json new file mode 100644 index 0000000..9194b08 --- /dev/null +++ b/js/package.json @@ -0,0 +1,41 @@ +{ + "name": "saver-webui-fe", + "version": "0.0.1", + "description": "Saver's Web UI frontend", + "main": "index.js", + "scripts": { + "start": "webpack-dev-server --open --config webpack.dev.js", + "build": "webpack --config webpack.prod.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/drbig/saver.git" + }, + "author": "Piotr S. Staszewski", + "license": "BSD-2-Clause", + "bugs": { + "url": "https://github.com/drbig/saver/issues" + }, + "homepage": "https://github.com/drbig/saver#readme", + "dependencies": { + "babel-core": "^6.26.3", + "babel-preset-env": "^1.7.0", + "babel-preset-react": "^6.24.1", + "clean-webpack-plugin": "^0.1.19", + "coffee-loader": "^0.9.0", + "coffeescript": "^2.3.1", + "css-loader": "^1.0.0", + "git-revision-webpack-plugin": "^3.0.3", + "handlebars": "^4.0.11", + "handlebars-loader": "^1.7.0", + "html-webpack-plugin": "^3.2.0", + "less": "^3.8.0", + "less-loader": "^4.1.0", + "react": "^16.4.1", + "react-dom": "^16.4.1", + "uglifyjs-webpack-plugin": "^1.2.7", + "webpack": "^3.10.0", + "webpack-dev-server": "^2.11.1", + "webpack-merge": "^4.1.1" + } +} diff --git a/js/src/index.coffee b/js/src/index.coffee new file mode 100644 index 0000000..9697025 --- /dev/null +++ b/js/src/index.coffee @@ -0,0 +1,12 @@ +require './index.less' +import React from 'react' +import {render} from 'react-dom' + + +class App extends React.Component + render: -> +
+ "Hello world!" +
+ +render , document.getElementById('app') diff --git a/js/src/index.hbs b/js/src/index.hbs new file mode 100644 index 0000000..aa1cc5a --- /dev/null +++ b/js/src/index.hbs @@ -0,0 +1,15 @@ + + + + + {{ htmlWebpackPlugin.options.title }} + + +
+ + + diff --git a/js/src/index.less b/js/src/index.less new file mode 100644 index 0000000..07e6b5e --- /dev/null +++ b/js/src/index.less @@ -0,0 +1,4 @@ +body { + background: black; + color: white; +} diff --git a/js/webpack.common.js b/js/webpack.common.js new file mode 100644 index 0000000..b4ad708 --- /dev/null +++ b/js/webpack.common.js @@ -0,0 +1,53 @@ +const path = require('path'); + +const CleanWebpackPlugin = require('clean-webpack-plugin'); +const GitRevisionPlugin = require('git-revision-webpack-plugin'); +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +const gitRevisionPlugin = new GitRevisionPlugin({ + versionCommand: 'describe --always' +}); + +module.exports = { + entry: { + app: './src/index.coffee' + }, + plugins: [ + new CleanWebpackPlugin(['dist']), + new HtmlWebpackPlugin({ + hash: true, + template: 'src/index.hbs', + title: 'Saver Web UI', + version: gitRevisionPlugin.version() + }) + ], + module: { + loaders: [ + { + test: /\.hbs$/, + use: ['handlebars-loader'] + }, + { + test: /\.coffee$/, + use: [ + { + loader: 'coffee-loader', + options: { + transpile: { + presets: ['env', 'react'] + } + } + } + ] + }, + { + test: /\.less$/, + use: ['css-loader', 'less-loader'] + } + ] + }, + output: { + filename: '[name].bundle.js', + path: path.resolve(__dirname, 'dist') + } +}; diff --git a/js/webpack.dev.js b/js/webpack.dev.js new file mode 100644 index 0000000..c840145 --- /dev/null +++ b/js/webpack.dev.js @@ -0,0 +1,9 @@ +const common = require('./webpack.common.js'); +const merge = require('webpack-merge'); + +module.exports = merge(common, { + devtool: 'inline-source-map', + devServer: { + contentBase: './dist' + } +}); diff --git a/js/webpack.prod.js b/js/webpack.prod.js new file mode 100644 index 0000000..4f69bc7 --- /dev/null +++ b/js/webpack.prod.js @@ -0,0 +1,10 @@ +const common = require('./webpack.common.js'); +const merge = require('webpack-merge'); + +const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); + +module.exports = merge(common, { + plugins: [ + new UglifyJSPlugin() + ] +}); From 951ac0ef25c7223cbabfc49afb8b89c7129829ac Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Tue, 24 Jul 2018 20:34:12 +0200 Subject: [PATCH 12/20] Remount current endpoints under /api --- webui.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/webui.go b/webui.go index 1917053..f7ec336 100644 --- a/webui.go +++ b/webui.go @@ -89,13 +89,14 @@ func startWebUI() { webuiLog = log.New(os.Stdout, "", log.Ltime|log.Lshortfile) r := mux.NewRouter() - r.HandleFunc("/list", handleGetList). + api := r.PathPrefix("/api/").Subrouter() + api.HandleFunc("/list", handleGetList). Methods(http.MethodGet) - r.HandleFunc("/{game}/backup", handleGetGameBackup). + api.HandleFunc("/{game}/backup", handleGetGameBackup). Methods(http.MethodGet) - r.HandleFunc("/{game}/restore/{id:[0-9]+}", handleGetGameRestore). + api.HandleFunc("/{game}/restore/{id:[0-9]+}", handleGetGameRestore). Methods(http.MethodGet) - r.HandleFunc("/{game}/delete/{from:[0-9]+}-{to:[0-9]+}", handleGetGameDelete). + api.HandleFunc("/{game}/delete/{from:[0-9]+}-{to:[0-9]+}", handleGetGameDelete). Methods(http.MethodGet) r.Use(loggingMw) http.Handle("/", r) From a603b82d3bee42c2c4b54404e871e2342fc53f3e Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Tue, 24 Jul 2018 20:34:32 +0200 Subject: [PATCH 13/20] Add serving of the FEnd content --- webui.go | 1 + 1 file changed, 1 insertion(+) diff --git a/webui.go b/webui.go index f7ec336..b99b58d 100644 --- a/webui.go +++ b/webui.go @@ -98,6 +98,7 @@ func startWebUI() { Methods(http.MethodGet) api.HandleFunc("/{game}/delete/{from:[0-9]+}-{to:[0-9]+}", handleGetGameDelete). Methods(http.MethodGet) + r.PathPrefix("/").Handler(http.FileServer(http.Dir("./js/dist/"))) r.Use(loggingMw) http.Handle("/", r) From de467b7981ea261f5a7c29188e5ce7864954bfe2 Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Tue, 24 Jul 2018 20:47:10 +0200 Subject: [PATCH 14/20] Make style loading work Ahh, the hammer it until it seems to work approach. --- js/package.json | 1 + js/webpack.common.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/js/package.json b/js/package.json index 9194b08..58d4976 100644 --- a/js/package.json +++ b/js/package.json @@ -33,6 +33,7 @@ "less-loader": "^4.1.0", "react": "^16.4.1", "react-dom": "^16.4.1", + "style-loader": "^0.21.0", "uglifyjs-webpack-plugin": "^1.2.7", "webpack": "^3.10.0", "webpack-dev-server": "^2.11.1", diff --git a/js/webpack.common.js b/js/webpack.common.js index b4ad708..546af5e 100644 --- a/js/webpack.common.js +++ b/js/webpack.common.js @@ -42,7 +42,7 @@ module.exports = { }, { test: /\.less$/, - use: ['css-loader', 'less-loader'] + use: ['style-loader', 'css-loader', 'less-loader'] } ] }, From ce039f5127e84be4adb78382e858af9e45722780 Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Mon, 30 Jul 2018 19:48:06 +0200 Subject: [PATCH 15/20] Fix scripts for build-prod and build-dev --- js/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/package.json b/js/package.json index 58d4976..5e8d3ee 100644 --- a/js/package.json +++ b/js/package.json @@ -5,7 +5,8 @@ "main": "index.js", "scripts": { "start": "webpack-dev-server --open --config webpack.dev.js", - "build": "webpack --config webpack.prod.js" + "build-prod": "webpack --config webpack.prod.js", + "build-dev": "webpack --config webpack.dev.js" }, "repository": { "type": "git", From b3505df015088803791a23f5a18279f562ee0d65 Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Mon, 30 Jul 2018 19:49:21 +0200 Subject: [PATCH 16/20] Guess how I'm coding this --- js/src/index.coffee | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/js/src/index.coffee b/js/src/index.coffee index 9697025..2b4a9d9 100644 --- a/js/src/index.coffee +++ b/js/src/index.coffee @@ -4,9 +4,39 @@ import {render} from 'react-dom' class App extends React.Component + constructor: (props) -> + super props + this.state = { + isLoaded: false, + error: null, + cfg: {}, + } + + componentDidMount: -> + fetch('/api/list') + .then((response) => response.json()) + .then( + (ok) => + this.setState({ + isLoaded: true, + cfg: ok, + }) + , + (error) => + this.setState({ + isLoaded: true, + error: error, + }) + ) + render: -> -
- "Hello world!" -
+ if this.state.error +
Error: {this.state.error.message}
+ else if !this.state.isLoaded +
Loading...
+ else +
+ Root: {this.state.cfg.Root} +
render , document.getElementById('app') From c7571f0af21ac4b5019c38bbea26e740d90e9a39 Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Mon, 30 Jul 2018 20:02:01 +0200 Subject: [PATCH 17/20] Small steps forward --- js/src/index.coffee | 11 ++++++++++- js/src/index.less | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/js/src/index.coffee b/js/src/index.coffee index 2b4a9d9..1e250af 100644 --- a/js/src/index.coffee +++ b/js/src/index.coffee @@ -31,12 +31,21 @@ class App extends React.Component render: -> if this.state.error -
Error: {this.state.error.message}
+
Error: {this.state.error.message}
else if !this.state.isLoaded
Loading...
else
Root: {this.state.cfg.Root} + {if this.state.cfg.Games.length < 1 +
No games defined. Please use CLI.
+ else +
    + {this.state.cfg.Games.map((game) => +
  • {game.Name}
  • + )} +
+ }
render , document.getElementById('app') diff --git a/js/src/index.less b/js/src/index.less index 07e6b5e..0c6142e 100644 --- a/js/src/index.less +++ b/js/src/index.less @@ -2,3 +2,12 @@ body { background: black; color: white; } + +div.footer { + font-size: 0.7em; + padding-top: 1em; +} + +div.error { + color: red; +} From 362eef8d096816e9adc1f92e6c50fa65231c8626 Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Mon, 30 Jul 2018 20:35:31 +0200 Subject: [PATCH 18/20] Yes, BEnd-engineer doing FEnd styling --- js/src/index.coffee | 43 ++++++++++++++++++++++++++++++++++++++----- js/src/index.less | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/js/src/index.coffee b/js/src/index.coffee index 1e250af..93ecc7f 100644 --- a/js/src/index.coffee +++ b/js/src/index.coffee @@ -3,6 +3,40 @@ import React from 'react' import {render} from 'react-dom' +class Game extends React.Component + constructor: (props) -> + super props + this.state = { + isExpanded: false, + isDetailed: false, + } + + toggleExpanded: -> + this.setState((prevState) => {isExpanded: !prevState.isExpanded}) + + toggleDetailed: -> + this.setState((prevState) => {isDetailed: !prevState.isDetailed}) + + render: -> + knob = if this.state.isExpanded + '[ - ]' + else + '[ + ]' + +
  • + this.toggleExpanded()}>{knob} + this.toggleDetailed()}>[i] + {this.props.game.Name} + {this.props.game.Stamp} + {if this.state.isDetailed +
    + Path: {this.props.game.Path} + Size: {this.props.game.Size} +
    + } +
  • + + class App extends React.Component constructor: (props) -> super props @@ -10,6 +44,7 @@ class App extends React.Component isLoaded: false, error: null, cfg: {}, + currentGame: null, } componentDidMount: -> @@ -31,19 +66,17 @@ class App extends React.Component render: -> if this.state.error -
    Error: {this.state.error.message}
    +
    Error: {this.state.error.message}
    else if !this.state.isLoaded
    Loading...
    else
    Root: {this.state.cfg.Root} {if this.state.cfg.Games.length < 1 -
    No games defined. Please use CLI.
    +
    No games defined. Please use CLI.
    else
      - {this.state.cfg.Games.map((game) => -
    • {game.Name}
    • - )} + {this.state.cfg.Games.map((game) => )}
    }
    diff --git a/js/src/index.less b/js/src/index.less index 0c6142e..542a39b 100644 --- a/js/src/index.less +++ b/js/src/index.less @@ -11,3 +11,44 @@ div.footer { div.error { color: red; } + +div.gameInfo { + display: block; + font-size: 0.8em; + padding: 0.5em; +} + +span { + display: inline-block; +} + +span.gameName { + width: 32em; +} + +span.gameStamp { + font-family: monospace; + font-size: 0.8em; +} + +a.knob { + display: inline-block; + width: 3.5em; + font-family: monospace; +} +a.knob:hover { + color: yellow; +} + +a.info { + display: inline-block; + width: 2.5em; + font-family: monospace; +} +a.info:hover { + color: yellow; +} + +li.gameInfo { + display: block; +} From 3e8047c478f381b1735e22ad5cebe50f3ff2ae0c Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Mon, 30 Jul 2018 20:52:43 +0200 Subject: [PATCH 19/20] Yeah, sensible UI ain't my strong suite --- js/src/index.coffee | 7 ++++--- js/src/index.less | 26 +++++++++++++++++--------- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/js/src/index.coffee b/js/src/index.coffee index 93ecc7f..fe1c498 100644 --- a/js/src/index.coffee +++ b/js/src/index.coffee @@ -23,10 +23,11 @@ class Game extends React.Component else '[ + ]' -
  • - this.toggleExpanded()}>{knob} +
  • + {knob} this.toggleDetailed()}>[i] - {this.props.game.Name} + ({this.props.game.Saves.length}) + this.toggleExpanded()}>{this.props.game.Name} {this.props.game.Stamp} {if this.state.isDetailed
    diff --git a/js/src/index.less b/js/src/index.less index 542a39b..f86454c 100644 --- a/js/src/index.less +++ b/js/src/index.less @@ -22,33 +22,41 @@ span { display: inline-block; } -span.gameName { +a.gameName { + display: inline-block; width: 32em; } +a.gameName:hover { + color: yellow; +} span.gameStamp { font-family: monospace; font-size: 0.8em; } -a.knob { - display: inline-block; - width: 3.5em; +span.knob { font-family: monospace; + padding-right: 0.5em; } -a.knob:hover { - color: yellow; + +span.savesCounter { + font-family: monospace; + display: inline-block; + width: 2em; + text-align: center; + padding-right: 0.5em; } a.info { - display: inline-block; - width: 2.5em; font-family: monospace; + padding-right: 0.5em; } a.info:hover { color: yellow; } -li.gameInfo { +li.saves { display: block; + padding: 0.5em; } From 5710d33d980c7776745b851ea287c80e526b6d40 Mon Sep 17 00:00:00 2001 From: "Piotr S. Staszewski" Date: Mon, 30 Jul 2018 21:10:43 +0200 Subject: [PATCH 20/20] This will take some time Save point here. --- js/src/index.coffee | 27 +++++++++++++++++++++++++-- js/src/index.less | 15 ++++++++++++++- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/js/src/index.coffee b/js/src/index.coffee index fe1c498..0918eca 100644 --- a/js/src/index.coffee +++ b/js/src/index.coffee @@ -3,6 +3,24 @@ import React from 'react' import {render} from 'react-dom' +class Save extends React.Component + constructor: (props) -> + super props + this.state = { + isDetailed: false, + } + + toggleDetailed: -> + this.setState((prevState) => {isDetailed: !prevState.isDetailed}) + + render: -> +
  • + [del] + {this.props.save.Stamp} + {this.props.save.Note} +
  • + + class Game extends React.Component constructor: (props) -> super props @@ -23,7 +41,7 @@ class Game extends React.Component else '[ + ]' -
  • +
  • {knob} this.toggleDetailed()}>[i] ({this.props.game.Saves.length}) @@ -35,6 +53,11 @@ class Game extends React.Component Size: {this.props.game.Size} } + {if this.state.isExpanded +
      + {this.props.game.Saves.map((save) => )} +
    + }
  • @@ -76,7 +99,7 @@ class App extends React.Component {if this.state.cfg.Games.length < 1
    No games defined. Please use CLI.
    else -
      +
        {this.state.cfg.Games.map((game) => )}
      } diff --git a/js/src/index.less b/js/src/index.less index f86454c..04a9d45 100644 --- a/js/src/index.less +++ b/js/src/index.less @@ -18,6 +18,11 @@ div.gameInfo { padding: 0.5em; } +ol.saves { + display: block; + padding: 0.5em; +} + span { display: inline-block; } @@ -30,12 +35,20 @@ a.gameName:hover { color: yellow; } +a.saveMain { + display: inline-block; + width: 35em; +} +a.saveMain:hover { + color: yellow; +} + span.gameStamp { font-family: monospace; font-size: 0.8em; } -span.knob { +.knob { font-family: monospace; padding-right: 0.5em; }