Brought over changes from zedshaw.games but need to do some more refinement.

master
Zed A. Shaw 4 days ago
parent 64af525bf8
commit 49946e7f26
  1. 52
      .air.toml
  2. 2
      .ozai.json
  3. 14
      Makefile
  4. 1
      admin/handlers.go
  5. 6
      api/auth.go
  6. 3
      api/handlers.go
  7. 8
      common/api.go
  8. 62
      common/web.go
  9. 6
      data/models.go
  10. 2
      go.mod
  11. 2
      pages/index.md
  12. 50
      static/input_style.css
  13. 103
      static/style.css
  14. 0
      tests/admin_ui_test.go
  15. 4
      views/admin/table/contents.html
  16. 2
      views/admin/table/view.html
  17. 23
      views/layouts/main.html

@ -1,52 +0,0 @@
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "webapp"
cmd = "make build"
delay = 1000
exclude_dir = ["assets", "pages", "static", "views", "public", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html", "css", "js"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
pre_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
stop_on_error = false
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
main_only = false
silent = false
time = false
[misc]
clean_on_exit = false
[proxy]
app_port = 7001
enabled = true
proxy_port = 7002
[screen]
clear_on_rebuild = false
keep_scroll = true

@ -14,7 +14,7 @@
"tailwind": {
"URL": "/tailwind",
"Command": "tailwindcss",
"Args": ["--input", "./static/input_style.css", "--output", "./static/style.css","--watch"]
"Args": ["--input", "./static/input_style.css", "--output", "./static/style.css","--watch=always"]
}
}
}

@ -1,9 +1,3 @@
GO_IS_STUPID_EXE=
ifeq '$(OS)' 'Windows_NT'
GO_IS_STUPID_EXE=.exe
endif
all: build
ifeq '$(OS)' 'Windows_NT'
powershell -ExecutionPolicy bypass "./tools/restart.ps1"
@ -18,12 +12,12 @@ site:
go tool ssgod
test: site
go test . -c -o runtests$(GO_IS_STUPID_EXE)
./runtests$(GO_IS_STUPID_EXE)
go test MY/webapp/tests -c
./tests.test
test_only:
go test . -c -o runtests$(GO_IS_STUPID_EXE)
./runtests$(GO_IS_STUPID_EXE) -test.run TestGamePage
go test . -c -o
./tests.test -test.run TestSomePage
migrate_up:
goose sqlite3 db.sqlite3 -dir migrations up

@ -169,4 +169,5 @@ func Setup(app *fiber.App) {
app.Post("/api/admin/new/table/:table", PostApiInsert)
app.Get("/api/admin/table/:table/:id", GetApiSelectOne)
app.Post("/api/admin/table/:table/:id", PostApiUpdate)
app.Delete("/api/admin/table/:table/:id", DeleteApi)
}

@ -15,6 +15,12 @@ import (
. "MY/webapp/common"
)
func GetApiAuthCheck(c *fiber.Ctx) error {
_, err := CheckAuthed(c, false)
// auth failure or not authed is determined by err, with nil meaning YES AUTHED
return c.JSON(fiber.Map{"is_authed": err == nil})
}
func GetApiLogout(c *fiber.Ctx) error {
err := LogoutUser(c)
if err != nil { return IfErrNil(err, c) }

@ -20,9 +20,10 @@ func Setup(app *fiber.App) {
})
// api/auth.go
app.Get("/api/authcheck", GetApiAuthCheck)
app.Get("/api/logout", GetApiLogout)
app.Post("/api/login", PostApiLogin)
app.Post("/api/register", PostApiRegister)
app.Post("/api/login", PostApiLogin)
}
func Shutdown() {

@ -48,7 +48,9 @@ func ReflectOnPost(typeOf reflect.Type, c *fiber.Ctx) (reflect.Value, error) {
result_val = reflect.New(typeOf)
result := result_val.Interface()
if err := c.BodyParser(result); err != nil {
if err := c.BodyParser(result)
err != nil {
log.Println(err);
return result_val, err
}
@ -56,7 +58,9 @@ func ReflectOnPost(typeOf reflect.Type, c *fiber.Ctx) (reflect.Value, error) {
var validate *validator.Validate
validate = validator.New(validator.WithRequiredStructEnabled())
if err := validate.Struct(result); err != nil {
err := validate.Struct(result)
if err != nil {
validationErrors := err.(validator.ValidationErrors)
log.Println(validationErrors)
return result_val, err

@ -1,70 +1,14 @@
package common
import (
"log"
"strings"
"io/fs"
"path/filepath"
"os"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/template/html/v2"
)
func Page(path string) (func(c *fiber.Ctx) error) {
page_id := strings.ReplaceAll(path, "/", "-") + "-page"
return func (c *fiber.Ctx) error {
return c.Render(path, fiber.Map{})
return c.Render(path, fiber.Map{"PageId": page_id})
}
}
func RenderPages(pages_path string, target string, layout string) {
engine := html.New(pages_path, ".html")
engine.Load()
err := filepath.WalkDir(pages_path,
func(path string, d fs.DirEntry, err error) error {
if !d.IsDir() {
if err != nil { return Fail(err, "path: %s", path); }
dir := filepath.Dir(path)
err = os.MkdirAll(dir, 0750)
if err != nil {
return Fail(err, "making dir %s", dir);
}
split_path := strings.Split(path, string(os.PathSeparator))[1:]
source_name := strings.Join(split_path, "/") // Render wants / even on windows
ext := filepath.Ext(source_name)
template_name, found := strings.CutSuffix(source_name, ext)
if found && ext == ".html" && template_name != layout {
prefixed_path := append([]string{target}, split_path...)
target_path := filepath.Join(prefixed_path...)
_, err := os.Stat(target_path)
if os.IsNotExist(err) {
target_dir := filepath.Dir(target_path)
log.Println("MAKING: ", target_dir)
os.MkdirAll(target_dir, 0750)
}
// TODO: compare time stamps and skip if not newer
out, err := os.OpenFile(target_path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
if err != nil { return Fail(err, "writing file %s", target_path) }
// generate a data-testid for all pages based on template name
page_id := strings.ReplaceAll(template_name, "/", "-") + "-page"
err = engine.Render(out, template_name, fiber.Map{"PageId": page_id}, layout)
if err != nil { return Fail(err, "failed to render %s", path) }
log.Printf("RENDER: %s -> %s", template_name, target_path)
out.Close()
}
}
return nil
})
if err != nil { log.Fatalf("can't walk content") }
}

@ -11,7 +11,7 @@ type Login struct {
}
type User struct {
Id int `db:"id" json:"id" validate:"numeric"`
Id int `db:"id" validate:"numeric"`
Username string `db:"username" validate:"required,max=30"`
Email string `db:"email" validate:"required,email,max=128"`
Password string `db:"password" validate:"required,min=8,max=64"`
@ -21,8 +21,8 @@ type User struct {
* Example of using the null library to do optional fields.
*/
type NullExample struct {
Id int `db:"id" json:"id" validate:"numeric"`
HasMaybe null.Int `db:"replying_to" json:"replying_to" validate:"omitempty,numeric"`
Id int `db:"id" validate:"numeric"`
HasMaybe null.Int `db:"replying_to" validate:"omitempty,numeric"`
}
func Models() map[string]reflect.Type {

@ -1,6 +1,6 @@
module MY/webapp
go 1.24.2
go 1.25.3
require (
github.com/BurntSushi/toml v1.5.0

@ -61,12 +61,14 @@ Then on Linux/OSX you want to delete the `.git` with this:
```shell
rm -rf .git
mv LICENSE LICENSE.old # it's MIT
```
And on Windows you use the ever more clear and totally easier:
```shell
rm -recurse -force .git
mv LICENSE LICENSE.old # it's MIT
```
Once you have that you can make it your own:

@ -6,15 +6,15 @@ body {
}
main {
@apply flex flex-col gap-4 p-0 min-h-screen bg-gray-100 text-gray-950 dark:bg-gray-900 dark:text-gray-50;
@apply flex flex-col justify-stretch items-center gap-4 p-0 min-h-screen bg-gray-200 text-black dark:bg-gray-900 dark:text-gray-50;
}
header {
@apply flex flex-col justify-stretch;
@apply flex flex-col justify-stretch items-center bg-gray-950;
}
footer {
@apply bg-gray-950 text-gray-50 text-lg flex p-1;
@apply bg-gray-950 text-gray-50 text-lg flex justify-center p-1;
}
@utility sticky-bottom {
@ -22,7 +22,11 @@ footer {
}
nav {
@apply flex justify-center items-center bg-gray-950 *:text-gray-50 *:flex-1 *:text-xl w-full justify-evenly;
@apply flex lg:w-4xl justify-center items-center *:text-gray-50 *:flex-1 *:text-xl w-full justify-evenly;
}
a {
@apply underline;
}
nav > a {
@ -44,23 +48,23 @@ pre > code {
h1 {
@apply text-6xl mb-2 mt-4;
@apply text-5xl mb-2 mt-4 sm:text-6xl;
}
h2 {
@apply text-5xl mb-2 mt-4;
@apply text-4xl mb-2 mt-4 sm:text-5xl;
}
h3 {
@apply text-4xl mb-2 mt-4;
@apply text-3xl mb-2 mt-4 sm:text-4xl;
}
h4 {
@apply text-3xl mb-2 mt-4;
@apply text-2xl mb-2 mt-4 sm:text-3xl;
}
h5 {
@apply text-2xl mb-2 mt-4;
@apply text-2xl mb-2 mt-4 sm:text-2xl;
}
details {
@ -68,7 +72,7 @@ details {
}
aside {
@apply p-2 rounded-lg bg-gray-100 text-gray-950;
@apply flex rounded-none flex-col p-4 gap-4 bg-gray-300 text-gray-950 text-2xl dark:text-gray-50 font-semibold dark:bg-gray-800;
}
aside > mark {
@ -187,8 +191,24 @@ block {
@apply flex flex-col p-4 gap-4;
}
block.center-horizontal {
@apply items-center;
}
block.center-vertical {
@apply justify-center;
}
bar {
@apply flex flex-row p-4 gap-4;
@apply flex flex-col sm:flex-row p-4 gap-4;
}
bar.center-horizontal {
@apply justify-center;
}
bar.center-vertical {
@apply items-center;
}
stack {
@ -232,3 +252,11 @@ table {
@utility table-row {
@apply *:border-2 *:border-black *:p-1 hover:bg-gray-100 hover:text-gray-950;
}
@utility two-panel {
@apply grid-rows-2 grid-cols-1 sm:grid-cols-2 sm:grid-rows-1;
}
@utility debug {
@apply !border-1 !border-red-900;
}

@ -36,6 +36,7 @@
--container-md: 28rem;
--container-lg: 32rem;
--container-xl: 36rem;
--container-4xl: 56rem;
--text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875);
--text-lg: 1.125rem;
@ -53,6 +54,7 @@
--text-6xl: 3.75rem;
--text-6xl--line-height: 1;
--font-weight-light: 300;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
@ -456,6 +458,9 @@
.bg-green-400\! {
background-color: var(--color-green-400) !important;
}
.\!p-1 {
padding: calc(var(--spacing) * 1) !important;
}
.\!p-4 {
padding: calc(var(--spacing) * 4) !important;
}
@ -582,6 +587,16 @@
}
}
}
.sm\:p-0 {
@media (width >= 40rem) {
padding: calc(var(--spacing) * 0);
}
}
.lg\:max-w-4xl {
@media (width >= 64rem) {
max-width: var(--container-4xl);
}
}
.dark\:bg-gray-900 {
@media (prefers-color-scheme: dark) {
background-color: var(--color-gray-900);
@ -604,10 +619,12 @@ main {
display: flex;
min-height: 100vh;
flex-direction: column;
align-items: center;
justify-content: stretch;
gap: calc(var(--spacing) * 4);
background-color: var(--color-gray-100);
background-color: var(--color-gray-200);
padding: calc(var(--spacing) * 0);
color: var(--color-gray-950);
color: var(--color-black);
@media (prefers-color-scheme: dark) {
background-color: var(--color-gray-900);
}
@ -618,10 +635,13 @@ main {
header {
display: flex;
flex-direction: column;
align-items: center;
justify-content: stretch;
background-color: var(--color-gray-950);
}
footer {
display: flex;
justify-content: center;
background-color: var(--color-gray-950);
padding: calc(var(--spacing) * 1);
font-size: var(--text-lg);
@ -634,7 +654,6 @@ nav {
align-items: center;
justify-content: center;
justify-content: space-evenly;
background-color: var(--color-gray-950);
:is(& > *) {
flex: 1;
}
@ -645,6 +664,12 @@ nav {
:is(& > *) {
color: var(--color-gray-50);
}
@media (width >= 64rem) {
width: var(--container-4xl);
}
}
a {
text-decoration-line: underline;
}
nav > a {
display: flex;
@ -671,34 +696,54 @@ pre > code {
padding: calc(var(--spacing) * 1);
}
h1 {
margin-top: calc(var(--spacing) * 4);
margin-bottom: calc(var(--spacing) * 2);
font-size: var(--text-6xl);
line-height: var(--tw-leading, var(--text-6xl--line-height));
}
h2 {
margin-top: calc(var(--spacing) * 4);
margin-bottom: calc(var(--spacing) * 2);
font-size: var(--text-5xl);
line-height: var(--tw-leading, var(--text-5xl--line-height));
@media (width >= 40rem) {
font-size: var(--text-6xl);
line-height: var(--tw-leading, var(--text-6xl--line-height));
}
}
h3 {
h2 {
margin-top: calc(var(--spacing) * 4);
margin-bottom: calc(var(--spacing) * 2);
font-size: var(--text-4xl);
line-height: var(--tw-leading, var(--text-4xl--line-height));
@media (width >= 40rem) {
font-size: var(--text-5xl);
line-height: var(--tw-leading, var(--text-5xl--line-height));
}
}
h4 {
h3 {
margin-top: calc(var(--spacing) * 4);
margin-bottom: calc(var(--spacing) * 2);
font-size: var(--text-3xl);
line-height: var(--tw-leading, var(--text-3xl--line-height));
@media (width >= 40rem) {
font-size: var(--text-4xl);
line-height: var(--tw-leading, var(--text-4xl--line-height));
}
}
h4 {
margin-top: calc(var(--spacing) * 4);
margin-bottom: calc(var(--spacing) * 2);
font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height));
@media (width >= 40rem) {
font-size: var(--text-3xl);
line-height: var(--tw-leading, var(--text-3xl--line-height));
}
}
h5 {
margin-top: calc(var(--spacing) * 4);
margin-bottom: calc(var(--spacing) * 2);
font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height));
@media (width >= 40rem) {
font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height));
}
}
details {
border-radius: var(--radius-lg);
@ -709,10 +754,23 @@ details {
color: var(--color-gray-950);
}
aside {
border-radius: var(--radius-lg);
background-color: var(--color-gray-100);
padding: calc(var(--spacing) * 2);
display: flex;
flex-direction: column;
gap: calc(var(--spacing) * 4);
border-radius: 0;
background-color: var(--color-gray-300);
padding: calc(var(--spacing) * 4);
font-size: var(--text-2xl);
line-height: var(--tw-leading, var(--text-2xl--line-height));
--tw-font-weight: var(--font-weight-semibold);
font-weight: var(--font-weight-semibold);
color: var(--color-gray-950);
@media (prefers-color-scheme: dark) {
background-color: var(--color-gray-800);
}
@media (prefers-color-scheme: dark) {
color: var(--color-gray-50);
}
}
aside > mark {
border-radius: var(--radius-sm);
@ -896,11 +954,26 @@ block {
gap: calc(var(--spacing) * 4);
padding: calc(var(--spacing) * 4);
}
block.center-horizontal {
align-items: center;
}
block.center-vertical {
justify-content: center;
}
bar {
display: flex;
flex-direction: row;
flex-direction: column;
gap: calc(var(--spacing) * 4);
padding: calc(var(--spacing) * 4);
@media (width >= 40rem) {
flex-direction: row;
}
}
bar.center-horizontal {
justify-content: center;
}
bar.center-vertical {
align-items: center;
}
stack {
display: grid;

@ -19,10 +19,10 @@
<th x-text="header"></th>
</template>
</tr>
<template x-for="item in contents" :key="item.id">
<template x-for="item in contents" :key="item.Id">
<tr class="table-row">
<td>
<a x-bind:href="'/admin/table/{{ .Table }}/' + item.id + '/'">
<a x-bind:href="'/admin/table/{{ .Table }}/' + item.Id + '/'">
#
</a>
</td>

@ -16,7 +16,7 @@
</middle>
<bottom>
<button type="button"><a href="/admin/table/{{ .Table }}/">Back</a></button>
<button class="hover:btn-alert" type="button" @click.prevent="ConfirmDelete('{{ .Table }}', item.id)">Delete</button>
<button class="hover:btn-alert" type="button" @click.prevent="ConfirmDelete('{{ .Table }}', item.Id)">Delete</button>
<button class="hover:btn-hover" type="submit">Update</button>
</bottom>
</card>

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta name="viewport" content="initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="author" content="Zed A. Shaw" />
<meta name="description" content="My Go learning project, which is a Twitch support thing." />
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
@ -16,7 +16,8 @@
<title>Go Web Dev Starter Kit</title>
</head>
<body id="top" data-testid="{{.PageId}}">
<header>
<header x-data="{auth: false}"
x-init="auth = await GetJson('/api/authcheck')">
<nav>
<a id="home" href="/">
<svg xmlns="http://www.w3.org/2000/svg"
@ -34,19 +35,19 @@
<use href="/icons/user-plus.svg#img" />
</svg>
</a>
<a id="login" href="/login/">
<svg xmlns="http://www.w3.org/2000/svg"
width="2rem"
height="2rem"
viewBox="0 0 2rem 2rem">
<use href="/icons/log-in.svg#img" />
</svg>
</a>
<template x-if="auth.is_authed">
<a id="logout" href="/api/logout">Logout</a>
</template>
<template x-if="!auth.is_authed">
<a id="login" href="/login/">Login</a>
</template>
</nav>
</header>
<main>
{{embed}}
<block class="w-full lg:max-w-4xl center-horizontal !p-1 sm:p-0">
{{embed}}
</block>
</main>
<footer class="sticky-bottom">

Loading…
Cancel
Save