Update. Add svelte admin frontend
This commit is contained in:
parent
17ab7c4929
commit
562186fa47
@ -4,16 +4,16 @@ tmp_dir = "tmp"
|
|||||||
|
|
||||||
[build]
|
[build]
|
||||||
args_bin = []
|
args_bin = []
|
||||||
bin = "./tmp/main"
|
bin = "tmp/server"
|
||||||
cmd = "go build -o ./tmp/main ."
|
cmd = "make"
|
||||||
delay = 1000
|
delay = 1000
|
||||||
exclude_dir = [
|
exclude_dir = [
|
||||||
"assets",
|
"assets",
|
||||||
"tmp",
|
"tmp",
|
||||||
"vendor",
|
"vendor",
|
||||||
"testdata",
|
"testdata",
|
||||||
|
"web/frontend/dist",
|
||||||
"web/frontend/node_modules",
|
"web/frontend/node_modules",
|
||||||
"web/static",
|
|
||||||
]
|
]
|
||||||
exclude_file = []
|
exclude_file = []
|
||||||
exclude_regex = ["_test.go"]
|
exclude_regex = ["_test.go"]
|
||||||
|
25
.gitignore
vendored
25
.gitignore
vendored
@ -23,3 +23,28 @@ go.work
|
|||||||
|
|
||||||
tmp
|
tmp
|
||||||
web/static/css/.DS_Store
|
web/static/css/.DS_Store
|
||||||
|
#
|
||||||
|
# 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?
|
||||||
|
5
Makefile
5
Makefile
@ -5,7 +5,7 @@ SVELTE_COMPONENTS = status
|
|||||||
|
|
||||||
SVELTE_TARGETS = $(addprefix $(FRONTEND)/public/build/,$(addsuffix .ts, $(SVELTE_COMPONENTS)))
|
SVELTE_TARGETS = $(addprefix $(FRONTEND)/public/build/,$(addsuffix .ts, $(SVELTE_COMPONENTS)))
|
||||||
|
|
||||||
.PHONY: $(TARGET)
|
.PHONY: $(TARGET) clean
|
||||||
.NOTPARALLEL:
|
.NOTPARALLEL:
|
||||||
|
|
||||||
$(TARGET): $(SVELTE_TARGETS)
|
$(TARGET): $(SVELTE_TARGETS)
|
||||||
@ -15,3 +15,6 @@ $(TARGET): $(SVELTE_TARGETS)
|
|||||||
$(SVELTE_TARGETS): $(SVELTE_SRC)
|
$(SVELTE_TARGETS): $(SVELTE_SRC)
|
||||||
$(info ===> BUILD frontend)
|
$(info ===> BUILD frontend)
|
||||||
cd $(FRONTEND) && npm install && npm run build
|
cd $(FRONTEND) && npm install && npm run build
|
||||||
|
|
||||||
|
clean:
|
||||||
|
@rm -rf tmp
|
||||||
|
100
internal/api/news.go
Normal file
100
internal/api/news.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createNewsItem(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
req := repository.CreateNewsEntryParams{}
|
||||||
|
if err := decode(r.Body, &req); err != nil {
|
||||||
|
handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := repository.GetRepository()
|
||||||
|
err := repo.CreateNewsEntry(r.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
handleError(err, http.StatusBadRequest, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readNewsItems(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
repo := repository.GetRepository()
|
||||||
|
items, err := repo.ListNews(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
handleError(err, http.StatusBadRequest, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("/api/news returned", "newscount", len(items))
|
||||||
|
rw.Header().Add("Content-Type", "application/json")
|
||||||
|
bw := bufio.NewWriter(rw)
|
||||||
|
defer bw.Flush()
|
||||||
|
|
||||||
|
if err := json.NewEncoder(bw).Encode(items); err != nil {
|
||||||
|
handleError(err, http.StatusInternalServerError, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readNewsItem(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
repo := repository.GetRepository()
|
||||||
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
handleError(err, http.StatusBadRequest, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item, err := repo.GetNewsEntry(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
handleError(err, http.StatusBadRequest, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rw.Header().Add("Content-Type", "application/json")
|
||||||
|
bw := bufio.NewWriter(rw)
|
||||||
|
defer bw.Flush()
|
||||||
|
|
||||||
|
if err := json.NewEncoder(bw).Encode(item); err != nil {
|
||||||
|
handleError(err, http.StatusInternalServerError, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateNewsItem(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
req := repository.UpdateNewsEntryParams{}
|
||||||
|
if err := decode(r.Body, &req); err != nil {
|
||||||
|
handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := repository.GetRepository()
|
||||||
|
err := repo.UpdateNewsEntry(r.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
handleError(err, http.StatusBadRequest, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteNewsItem(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
repo := repository.GetRepository()
|
||||||
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
handleError(err, http.StatusBadRequest, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = repo.DeleteNewsEntry(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
handleError(err, http.StatusBadRequest, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,10 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ErrorResponse struct {
|
type ErrorResponse struct {
|
||||||
@ -24,6 +19,12 @@ func MountApiEndpoints(r *http.ServeMux) {
|
|||||||
r.HandleFunc("GET /api/news/{id}", readNewsItem)
|
r.HandleFunc("GET /api/news/{id}", readNewsItem)
|
||||||
r.HandleFunc("PATCH /api/news/", updateNewsItem)
|
r.HandleFunc("PATCH /api/news/", updateNewsItem)
|
||||||
r.HandleFunc("DELETE /api/news/{id}", deleteNewsItem)
|
r.HandleFunc("DELETE /api/news/{id}", deleteNewsItem)
|
||||||
|
|
||||||
|
r.HandleFunc("POST /api/retailers/", createRetailerItem)
|
||||||
|
r.HandleFunc("GET /api/retailers/", readRetailerItems)
|
||||||
|
r.HandleFunc("GET /api/retailers/{id}", readRetailerItem)
|
||||||
|
r.HandleFunc("PATCH /api/retailers/", updateRetailerItem)
|
||||||
|
r.HandleFunc("DELETE /api/retailers/{id}", deleteRetailerItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleError(err error, statusCode int, rw http.ResponseWriter) {
|
func handleError(err error, statusCode int, rw http.ResponseWriter) {
|
||||||
@ -41,91 +42,3 @@ func decode(r io.Reader, val any) error {
|
|||||||
dec.DisallowUnknownFields()
|
dec.DisallowUnknownFields()
|
||||||
return dec.Decode(val)
|
return dec.Decode(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createNewsItem(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
req := repository.CreateNewsEntryParams{}
|
|
||||||
if err := decode(r.Body, &req); err != nil {
|
|
||||||
handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
repo := repository.GetRepository()
|
|
||||||
err := repo.CreateNewsEntry(r.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
handleError(err, http.StatusBadRequest, rw)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func readNewsItems(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
repo := repository.GetRepository()
|
|
||||||
items, err := repo.ListNews(r.Context())
|
|
||||||
if err != nil {
|
|
||||||
handleError(err, http.StatusBadRequest, rw)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
slog.Debug("/api/news returned", "newscount", len(items))
|
|
||||||
rw.Header().Add("Content-Type", "application/json")
|
|
||||||
bw := bufio.NewWriter(rw)
|
|
||||||
defer bw.Flush()
|
|
||||||
|
|
||||||
if err := json.NewEncoder(bw).Encode(items); err != nil {
|
|
||||||
handleError(err, http.StatusInternalServerError, rw)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func readNewsItem(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
repo := repository.GetRepository()
|
|
||||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
handleError(err, http.StatusBadRequest, rw)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
item, err := repo.GetNewsEntry(r.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
handleError(err, http.StatusBadRequest, rw)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
rw.Header().Add("Content-Type", "application/json")
|
|
||||||
bw := bufio.NewWriter(rw)
|
|
||||||
defer bw.Flush()
|
|
||||||
|
|
||||||
if err := json.NewEncoder(bw).Encode(item); err != nil {
|
|
||||||
handleError(err, http.StatusInternalServerError, rw)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateNewsItem(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
req := repository.UpdateNewsEntryParams{}
|
|
||||||
if err := decode(r.Body, &req); err != nil {
|
|
||||||
handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
repo := repository.GetRepository()
|
|
||||||
err := repo.UpdateNewsEntry(r.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
handleError(err, http.StatusBadRequest, rw)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func deleteNewsItem(rw http.ResponseWriter, r *http.Request) {
|
|
||||||
repo := repository.GetRepository()
|
|
||||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
handleError(err, http.StatusBadRequest, rw)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = repo.DeleteNewsEntry(r.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
handleError(err, http.StatusBadRequest, rw)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
100
internal/api/retailer.go
Normal file
100
internal/api/retailer.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createRetailerItem(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
req := repository.CreateNewsEntryParams{}
|
||||||
|
if err := decode(r.Body, &req); err != nil {
|
||||||
|
handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := repository.GetRepository()
|
||||||
|
err := repo.CreateNewsEntry(r.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
handleError(err, http.StatusBadRequest, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readRetailerItems(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
repo := repository.GetRepository()
|
||||||
|
items, err := repo.ListRetailers(r.Context())
|
||||||
|
if err != nil {
|
||||||
|
handleError(err, http.StatusBadRequest, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("/api/retailers returned", "count", len(items))
|
||||||
|
rw.Header().Add("Content-Type", "application/json")
|
||||||
|
bw := bufio.NewWriter(rw)
|
||||||
|
defer bw.Flush()
|
||||||
|
|
||||||
|
if err := json.NewEncoder(bw).Encode(items); err != nil {
|
||||||
|
handleError(err, http.StatusInternalServerError, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readRetailerItem(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
repo := repository.GetRepository()
|
||||||
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
handleError(err, http.StatusBadRequest, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
item, err := repo.GetNewsEntry(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
handleError(err, http.StatusBadRequest, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rw.Header().Add("Content-Type", "application/json")
|
||||||
|
bw := bufio.NewWriter(rw)
|
||||||
|
defer bw.Flush()
|
||||||
|
|
||||||
|
if err := json.NewEncoder(bw).Encode(item); err != nil {
|
||||||
|
handleError(err, http.StatusInternalServerError, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateRetailerItem(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
req := repository.UpdateNewsEntryParams{}
|
||||||
|
if err := decode(r.Body, &req); err != nil {
|
||||||
|
handleError(fmt.Errorf("parsing request body failed: %w", err), http.StatusBadRequest, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := repository.GetRepository()
|
||||||
|
err := repo.UpdateNewsEntry(r.Context(), req)
|
||||||
|
if err != nil {
|
||||||
|
handleError(err, http.StatusBadRequest, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteRetailerItem(rw http.ResponseWriter, r *http.Request) {
|
||||||
|
repo := repository.GetRepository()
|
||||||
|
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
handleError(err, http.StatusBadRequest, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = repo.DeleteNewsEntry(r.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
handleError(err, http.StatusBadRequest, rw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -13,11 +14,12 @@ func AdminHandler() http.HandlerFunc {
|
|||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
tpl := template.Must(template.ParseFS(web.Templates, "templates/admin.html", "templates/base.html"))
|
tpl := template.Must(template.ParseFS(web.Templates, "templates/admin.html", "templates/base.html"))
|
||||||
viteFragment, err := vite.HTMLFragment(vite.Config{
|
viteFragment, err := vite.HTMLFragment(vite.Config{
|
||||||
FS: web.StaticAssets,
|
FS: web.DistFS(),
|
||||||
IsDev: false,
|
IsDev: false,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Error instantiating vite fragment", http.StatusInternalServerError)
|
msg := fmt.Errorf("error instantiating vite fragment: %v", err)
|
||||||
|
http.Error(w, msg.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
11
main.go
11
main.go
@ -11,12 +11,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/api"
|
||||||
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/auth"
|
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/auth"
|
||||||
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/handlers"
|
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/handlers"
|
||||||
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/middleware"
|
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/middleware"
|
||||||
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository"
|
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository"
|
||||||
"git.clustercockpit.org/moebiusband/go-http-skeleton/web"
|
"git.clustercockpit.org/moebiusband/go-http-skeleton/web"
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
)
|
)
|
||||||
@ -116,8 +118,14 @@ func main() {
|
|||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
q := repository.GetRepository()
|
q := repository.GetRepository()
|
||||||
|
hash, err := bcrypt.GenerateFromPassword([]byte(parts[1]), bcrypt.DefaultCost)
|
||||||
|
password := string(hash)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("Error while encrypting new user password")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
if err := q.CreateUser(ctx, repository.CreateUserParams{
|
if err := q.CreateUser(ctx, repository.CreateUserParams{
|
||||||
UserName: parts[0], UserPass: &parts[1],
|
UserName: parts[0], UserPass: &password,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
slog.Error("Add User: Could not add new user authentication", "username", parts[0], "error", err.Error())
|
slog.Error("Add User: Could not add new user authentication", "username", parts[0], "error", err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -215,6 +223,7 @@ func main() {
|
|||||||
})
|
})
|
||||||
})))
|
})))
|
||||||
|
|
||||||
|
api.MountApiEndpoints(mux)
|
||||||
mux.HandleFunc("GET /", handlers.RootHandler())
|
mux.HandleFunc("GET /", handlers.RootHandler())
|
||||||
|
|
||||||
securedMux := http.NewServeMux()
|
securedMux := http.NewServeMux()
|
||||||
|
24
web/frontend/.gitignore
vendored
24
web/frontend/.gitignore
vendored
@ -1,24 +0,0 @@
|
|||||||
# 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?
|
|
@ -1,45 +1,139 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import svelteLogo from "./assets/svelte.svg";
|
import Table from "./lib/Table.svelte";
|
||||||
import Counter from "./lib/Counter.svelte";
|
import GenericForm from "./lib/GenericForm.svelte";
|
||||||
|
|
||||||
|
type Retailer = {
|
||||||
|
id: number;
|
||||||
|
shopname: string;
|
||||||
|
url: string;
|
||||||
|
country: string;
|
||||||
|
display: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
let records: Retailer[] = $state([]);
|
||||||
|
let columns: string[] = ["shopname", "country", "url"];
|
||||||
|
let activeTab = $state("invoice");
|
||||||
|
|
||||||
|
async function fetchData(endpoint: string) {
|
||||||
|
const res = await fetch("http://localhost:8080/api/" + endpoint);
|
||||||
|
records = await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (activeTab === "retailers") {
|
||||||
|
fetchData("retailers/");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let showModal = $state(false);
|
||||||
|
|
||||||
|
function openModal() {
|
||||||
|
showModal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
showModal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let formdata: Retailer = {
|
||||||
|
shopname: "",
|
||||||
|
url: "",
|
||||||
|
id: 0,
|
||||||
|
country: "",
|
||||||
|
display: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
function submitUserForm(data: Retailer) {
|
||||||
|
closeModal();
|
||||||
|
console.log("User Form Submitted:", data);
|
||||||
|
}
|
||||||
|
function removeRowHandler(id: Number) {
|
||||||
|
console.log("Remove Record:", id);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Bootstrap Modal -->
|
||||||
|
<div
|
||||||
|
class="modal fade {showModal ? 'show' : ''}"
|
||||||
|
tabindex="-1"
|
||||||
|
role="dialog"
|
||||||
|
style="display: {showModal ? 'block' : 'none'};"
|
||||||
|
>
|
||||||
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Add Retailer</h5>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<GenericForm formData={formdata} onSubmit={submitUserForm}>
|
||||||
|
{#each columns as col}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for={col}>{col}:</label>
|
||||||
|
<input
|
||||||
|
id={col}
|
||||||
|
type="text"
|
||||||
|
bind:value={formdata[col as keyof Retailer]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</GenericForm>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer justify-content-end">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick={closeModal}
|
||||||
|
>Close</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<div>
|
<ul class="nav nav-tabs">
|
||||||
<a href="https://svelte.dev" target="_blank" rel="noreferrer">
|
<li class="nav-item">
|
||||||
<img src={svelteLogo} class="logo svelte" alt="Svelte Logo" />
|
<a
|
||||||
</a>
|
class="nav-link {activeTab === 'invoice' ? 'active' : ''}"
|
||||||
|
href="#"
|
||||||
|
onclick={() => (activeTab = "invoice")}>Invoices</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
class="nav-link {activeTab === 'retailers' ? 'active' : ''}"
|
||||||
|
href="#"
|
||||||
|
onclick={() => (activeTab = "retailers")}>Retailers</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a
|
||||||
|
class="nav-link {activeTab === 'news' ? 'active' : ''}"
|
||||||
|
href="#"
|
||||||
|
onclick={() => (activeTab = "news")}>News</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="container-fluid">
|
||||||
|
{#if activeTab === "invoice"}
|
||||||
|
<p>Invoice list</p>
|
||||||
|
{/if}
|
||||||
|
{#if activeTab === "retailers"}
|
||||||
|
<div class="row justify-content-start">
|
||||||
|
<div class="col-2 mt-3">
|
||||||
|
<button
|
||||||
|
onclick={openModal}
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-primary">Add retailer</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<Table {columns} {records} {removeRowHandler} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if activeTab === "news"}
|
||||||
|
<p>news</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<h1>Vite + Svelte</h1>
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<Counter />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Check out <a
|
|
||||||
href="https://github.com/sveltejs/kit#readme"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer">SvelteKit</a
|
|
||||||
>, the official Svelte app framework powered by Vite!
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="read-the-docs">Click on the Vite and Svelte logos to learn more</p>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
|
||||||
.logo {
|
|
||||||
height: 6em;
|
|
||||||
padding: 1.5em;
|
|
||||||
will-change: filter;
|
|
||||||
transition: filter 300ms;
|
|
||||||
}
|
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
|
||||||
}
|
|
||||||
.logo.svelte:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #ff3e00aa);
|
|
||||||
}
|
|
||||||
.read-the-docs {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
14
web/frontend/src/lib/GenericForm.svelte
Normal file
14
web/frontend/src/lib/GenericForm.svelte
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<script lang="ts" generics="T">
|
||||||
|
export let formData: T;
|
||||||
|
export let onSubmit: (data: T) => void;
|
||||||
|
|
||||||
|
function handleSubmit(event: SubmitEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
onSubmit(formData);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form on:submit|preventDefault={handleSubmit}>
|
||||||
|
<slot />
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
48
web/frontend/src/lib/Table.svelte
Normal file
48
web/frontend/src/lib/Table.svelte
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
interface Props {
|
||||||
|
columns: string[];
|
||||||
|
records: any;
|
||||||
|
removeRowHandler: (id: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { columns, records, removeRowHandler }: Props = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{#each columns as col}
|
||||||
|
<th scope="col">{col}</th>
|
||||||
|
{/each}
|
||||||
|
<th scope="col">edit</th>
|
||||||
|
<th scope="col">remove</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each records as row}
|
||||||
|
<tr>
|
||||||
|
{#each columns as col}
|
||||||
|
<td>{row[col]}</td>
|
||||||
|
{/each}
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
onclick={() => removeRowHandler(row.id)}
|
||||||
|
class="btn btn-outline-success align-items-center"
|
||||||
|
aria-label="Edit record"
|
||||||
|
>
|
||||||
|
<i class="bi bi-pencil"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
onclick={() => removeRowHandler(row.id)}
|
||||||
|
class="btn btn-outline-danger align-items-center"
|
||||||
|
aria-label="Remove record"
|
||||||
|
>
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
@ -4,4 +4,7 @@ import { svelte } from '@sveltejs/vite-plugin-svelte'
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [svelte()],
|
plugins: [svelte()],
|
||||||
|
build: {
|
||||||
|
manifest: true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
@ -2,6 +2,8 @@ package web
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
@ -19,9 +21,17 @@ type PageData struct {
|
|||||||
//go:embed templates
|
//go:embed templates
|
||||||
var Templates embed.FS
|
var Templates embed.FS
|
||||||
|
|
||||||
//go:embed frontend/dist
|
//go:embed all:frontend/dist
|
||||||
var StaticAssets embed.FS
|
var StaticAssets embed.FS
|
||||||
|
|
||||||
|
func DistFS() fs.FS {
|
||||||
|
efs, err := fs.Sub(StaticAssets, "frontend/dist")
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("unable to serve frontend: %v", err))
|
||||||
|
}
|
||||||
|
return efs
|
||||||
|
}
|
||||||
|
|
||||||
func RenderTemplate(w http.ResponseWriter, name string, data PageData) error {
|
func RenderTemplate(w http.ResponseWriter, name string, data PageData) error {
|
||||||
tpl := template.Must(template.ParseFS(Templates, "templates/"+name+".html", "templates/base.html"))
|
tpl := template.Must(template.ParseFS(Templates, "templates/"+name+".html", "templates/base.html"))
|
||||||
return tpl.ExecuteTemplate(w, "base", data)
|
return tpl.ExecuteTemplate(w, "base", data)
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
{{define "vite"}} {{ .Vite.Tags }} {{end}} {{define "content"}}
|
{{define "vite"}} {{ .Vite.Tags }} {{end}} {{define "content"}}
|
||||||
<div id="root"></div>
|
<div id="app"></div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user