Update. Add svelte admin frontend
This commit is contained in:
parent
17ab7c4929
commit
562186fa47
@ -4,16 +4,16 @@ tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./tmp/main"
|
||||
cmd = "go build -o ./tmp/main ."
|
||||
bin = "tmp/server"
|
||||
cmd = "make"
|
||||
delay = 1000
|
||||
exclude_dir = [
|
||||
"assets",
|
||||
"tmp",
|
||||
"vendor",
|
||||
"testdata",
|
||||
"web/frontend/dist",
|
||||
"web/frontend/node_modules",
|
||||
"web/static",
|
||||
]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
|
25
.gitignore
vendored
25
.gitignore
vendored
@ -23,3 +23,28 @@ go.work
|
||||
|
||||
tmp
|
||||
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)))
|
||||
|
||||
.PHONY: $(TARGET)
|
||||
.PHONY: $(TARGET) clean
|
||||
.NOTPARALLEL:
|
||||
|
||||
$(TARGET): $(SVELTE_TARGETS)
|
||||
@ -15,3 +15,6 @@ $(TARGET): $(SVELTE_TARGETS)
|
||||
$(SVELTE_TARGETS): $(SVELTE_SRC)
|
||||
$(info ===> BUILD frontend)
|
||||
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
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.clustercockpit.org/moebiusband/go-http-skeleton/internal/repository"
|
||||
)
|
||||
|
||||
type ErrorResponse struct {
|
||||
@ -24,6 +19,12 @@ func MountApiEndpoints(r *http.ServeMux) {
|
||||
r.HandleFunc("GET /api/news/{id}", readNewsItem)
|
||||
r.HandleFunc("PATCH /api/news/", updateNewsItem)
|
||||
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) {
|
||||
@ -41,91 +42,3 @@ func decode(r io.Reader, val any) error {
|
||||
dec.DisallowUnknownFields()
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
@ -13,11 +14,12 @@ func AdminHandler() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
tpl := template.Must(template.ParseFS(web.Templates, "templates/admin.html", "templates/base.html"))
|
||||
viteFragment, err := vite.HTMLFragment(vite.Config{
|
||||
FS: web.StaticAssets,
|
||||
FS: web.DistFS(),
|
||||
IsDev: false,
|
||||
})
|
||||
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
|
||||
}
|
||||
|
||||
|
11
main.go
11
main.go
@ -11,12 +11,14 @@ import (
|
||||
"strings"
|
||||
"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/handlers"
|
||||
"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/web"
|
||||
"github.com/joho/godotenv"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
@ -116,8 +118,14 @@ func main() {
|
||||
|
||||
ctx := context.Background()
|
||||
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{
|
||||
UserName: parts[0], UserPass: &parts[1],
|
||||
UserName: parts[0], UserPass: &password,
|
||||
}); err != nil {
|
||||
slog.Error("Add User: Could not add new user authentication", "username", parts[0], "error", err.Error())
|
||||
os.Exit(1)
|
||||
@ -215,6 +223,7 @@ func main() {
|
||||
})
|
||||
})))
|
||||
|
||||
api.MountApiEndpoints(mux)
|
||||
mux.HandleFunc("GET /", handlers.RootHandler())
|
||||
|
||||
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">
|
||||
import svelteLogo from "./assets/svelte.svg";
|
||||
import Counter from "./lib/Counter.svelte";
|
||||
import Table from "./lib/Table.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>
|
||||
|
||||
<!-- 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>
|
||||
<div>
|
||||
<a href="https://svelte.dev" target="_blank" rel="noreferrer">
|
||||
<img src={svelteLogo} class="logo svelte" alt="Svelte Logo" />
|
||||
</a>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<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/
|
||||
export default defineConfig({
|
||||
plugins: [svelte()],
|
||||
build: {
|
||||
manifest: true
|
||||
}
|
||||
})
|
||||
|
@ -2,6 +2,8 @@ package web
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"text/template"
|
||||
|
||||
@ -19,9 +21,17 @@ type PageData struct {
|
||||
//go:embed templates
|
||||
var Templates embed.FS
|
||||
|
||||
//go:embed frontend/dist
|
||||
//go:embed all:frontend/dist
|
||||
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 {
|
||||
tpl := template.Must(template.ParseFS(Templates, "templates/"+name+".html", "templates/base.html"))
|
||||
return tpl.ExecuteTemplate(w, "base", data)
|
||||
|
@ -1,3 +1,3 @@
|
||||
{{define "vite"}} {{ .Vite.Tags }} {{end}} {{define "content"}}
|
||||
<div id="root"></div>
|
||||
<div id="app"></div>
|
||||
{{end}}
|
||||
|
Loading…
x
Reference in New Issue
Block a user