Brought over most of the changes from go-web-starter-kit.

This commit is contained in:
Zed A. Shaw 2025-08-31 11:23:12 -04:00
parent 01fcb964be
commit 8d70b57299
11 changed files with 143 additions and 57 deletions

1
.gitignore vendored
View file

@ -30,3 +30,4 @@ coverage/*
config.toml config.toml
public public
*.idx *.idx
*.sqlite3

View file

@ -48,7 +48,7 @@ tailwind_install:
chmod oug+x tailwindcss-linux-x64 chmod oug+x tailwindcss-linux-x64
sudo mv tailwindcss-linux-x64 /usr/local/bin/tailwindcss sudo mv tailwindcss-linux-x64 /usr/local/bin/tailwindcss
dev: dev: tailwind site
go tool air -build.stop_on_error "true" go tool air -build.stop_on_error "true"
coverage: coverage:

View file

@ -8,19 +8,22 @@
<meta name="description" content="My Go learning project, which is a Twitch support thing." /> <meta name="description" content="My Go learning project, which is a Twitch support thing." />
<link rel="icon" href="/favicon.ico" type="image/x-icon" /> <link rel="icon" href="/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">
<!-- all alpine plugins come first to register into alpine:init event -->
<script defer src="/js/alpine-intersect.js"></script>
<!-- then alpine runs, triggers init, and tada you get no-build plugins -->
<script defer src="/js/alpine.js"></script> <script defer src="/js/alpine.js"></script>
<script src="/js/code.js"></script> <script src="/js/code.js"></script>
<title>ZedShaw.games</title> <title>ZedShaw.games</title>
</head> </head>
<body data-testid="{{.PageId}}"> <body id="top" data-testid="{{.PageId}}">
<header> <header class="p-4">
<nav> <nav>
<a id="home" href="/"> <a id="home" href="/">
<svg xmlns="http://www.w3.org/2000/svg" <svg xmlns="http://www.w3.org/2000/svg"
width="2rem" width="24"
height="2rem" height="24"
viewBox="0 0 2rem 2rem"> viewBox="0 0 24 24">
<use href="/icons/home.svg#home" /> <use href="/icons/home.svg#home"><use>
</svg> </svg>
</a> </a>
<a id="live" href="/live/">Live</a> <a id="live" href="/live/">Live</a>

View file

@ -2,7 +2,7 @@
@import "./theme.css" layer(theme); @import "./theme.css" layer(theme);
body { body {
@apply text-gray-950 dark:text-gray-50; @apply bg-gray-100 text-gray-950 dark:text-gray-50 dark:bg-gray-950;
} }
main { main {
@ -10,11 +10,15 @@ main {
} }
header { header {
@apply flex flex-col justify-stretch; @apply flex justify-stretch;
} }
nav { nav {
@apply flex bg-gray-950 *:text-gray-50 *:flex-1 *:text-xl p-6; @apply flex justify-center items-center bg-gray-950 *:text-gray-50 *:flex-1 *:text-xl w-full justify-evenly;
}
nav > a {
@apply flex justify-center items-center pt-1;
} }
code { code {
@ -23,7 +27,7 @@ code {
} }
pre { pre {
@apply bg-gray-950 rounded-lg border-1 border-gray-300 mb-4 p-1; @apply bg-gray-950 border-1 border-gray-600 mb-4 p-1;
} }
pre > code { pre > code {
@ -34,6 +38,10 @@ footer {
@apply bg-gray-950 text-gray-50 text-lg flex p-6; @apply bg-gray-950 text-gray-50 text-lg flex p-6;
} }
@utility sticky-bottom {
@apply w-full sticky bottom-0 left-0;
}
h1 { h1 {
@apply text-6xl mb-2 mt-4; @apply text-6xl mb-2 mt-4;
} }
@ -118,6 +126,10 @@ button {
@apply rounded-sm shadow-sm bg-gray-600 text-gray-50 dark:bg-gray-300 dark:text-gray-950 p-3; @apply rounded-sm shadow-sm bg-gray-600 text-gray-50 dark:bg-gray-300 dark:text-gray-950 p-3;
} }
blockquote {
@apply border-l-5 border-gray-800 dark:border-black bg-gray-200 text-black dark:bg-gray-700 dark:text-white p-2;
}
@utility btn-hover { @utility btn-hover {
@apply !bg-gray-900 !text-gray-50; @apply !bg-gray-900 !text-gray-50;
} }
@ -171,7 +183,7 @@ shape.video {
} }
block { block {
@apply flex flex-col p-4 mb-10 gap-4; @apply flex flex-col p-4 gap-4;
} }
bar { bar {

View file

@ -0,0 +1 @@
(()=>{function o(e){e.directive("intersect",e.skipDuringClone((t,{value:i,expression:l,modifiers:n},{evaluateLater:r,cleanup:c})=>{let s=r(l),a={rootMargin:x(n),threshold:f(n)},u=new IntersectionObserver(d=>{d.forEach(h=>{h.isIntersecting!==(i==="leave")&&(s(),n.includes("once")&&u.disconnect())})},a);u.observe(t),c(()=>{u.disconnect()})}))}function f(e){if(e.includes("full"))return .99;if(e.includes("half"))return .5;if(!e.includes("threshold"))return 0;let t=e[e.indexOf("threshold")+1];return t==="100"?1:t==="0"?0:Number(`.${t}`)}function p(e){let t=e.match(/^(-?[0-9]+)(px|%)?$/);return t?t[1]+(t[2]||"px"):void 0}function x(e){let t="margin",i="0px 0px 0px 0px",l=e.indexOf(t);if(l===-1)return i;let n=[];for(let r=1;r<5;r++)n.push(p(e[l+r]||""));return n=n.filter(r=>r!==void 0),n.length?n.join(" ").trim():i}document.addEventListener("alpine:init",()=>{window.Alpine.plugin(o)});})();

View file

@ -4,7 +4,7 @@ class PaginateTable {
this.items = []; this.items = [];
this.url = url; this.url = url;
this.headers = []; this.headers = [];
this.search_query="" this.search_query="";
} }
async contents() { async contents() {
@ -31,19 +31,43 @@ class PaginateTable {
} }
} }
class GetJson { class ForeverScroll {
constructor(url) { constructor(url) {
this.item; this.page = 0;
this.items = [];
this.url = url; this.url = url;
this.end = false;
} }
async item() { async init() {
const resp = await fetch(`${this.url}`); const resp = await fetch(this.url);
console.assert(resp.status == 200, "failed to get it"); console.assert(resp.status == 200, "failed to get it");
this.item = await resp.json(); const items = await resp.json();
return this.item; if(items) this.items = items;
} }
async load() {
this.page += 1
let url = `${this.url}?page=${this.page}`;
const resp = await fetch(url);
console.assert(resp.status == 200, "failed to get it");
const items = await resp.json();
if(items) {
this.items.push(...items);
} else {
this.end = true;
}
}
}
const GetJson = async (url) => {
const resp = await fetch(url);
console.assert(resp.status == 200, "failed to get it");
return await resp.json();
} }
const ConfirmDelete = async (table, obj_id) => { const ConfirmDelete = async (table, obj_id) => {
@ -55,3 +79,14 @@ const ConfirmDelete = async (table, obj_id) => {
return false; return false;
} }
} }
const UrlId = () => {
let url = new URL(window.location.href);
let parts = url.pathname.split("/");
if(window.location.href.endsWith("/")) {
return parts[parts.length - 2];
} else {
return parts[parts.length - 1];
}
}

View file

@ -29,6 +29,7 @@
--color-gray-900: oklch(21% 0.0 264.665); --color-gray-900: oklch(21% 0.0 264.665);
--color-gray-950: oklch(13% 0.0 261.692); --color-gray-950: oklch(13% 0.0 261.692);
--color-black: #000; --color-black: #000;
--color-white: #fff;
--spacing: 0.25rem; --spacing: 0.25rem;
--container-xs: 20rem; --container-xs: 20rem;
--container-sm: 24rem; --container-sm: 24rem;
@ -477,9 +478,6 @@
.\!p-4 { .\!p-4 {
padding: calc(var(--spacing) * 4) !important; padding: calc(var(--spacing) * 4) !important;
} }
.p-0 {
padding: calc(var(--spacing) * 0);
}
.p-0\! { .p-0\! {
padding: calc(var(--spacing) * 0) !important; padding: calc(var(--spacing) * 0) !important;
} }
@ -640,7 +638,11 @@
} }
@layer theme; @layer theme;
body { body {
background-color: var(--color-gray-100);
color: var(--color-gray-950); color: var(--color-gray-950);
@media (prefers-color-scheme: dark) {
background-color: var(--color-gray-950);
}
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
color: var(--color-gray-50); color: var(--color-gray-50);
} }
@ -662,13 +664,15 @@ main {
} }
header { header {
display: flex; display: flex;
flex-direction: column;
justify-content: stretch; justify-content: stretch;
} }
nav { nav {
display: flex; display: flex;
width: 100%;
align-items: center;
justify-content: center;
justify-content: space-evenly;
background-color: var(--color-gray-950); background-color: var(--color-gray-950);
padding: calc(var(--spacing) * 6);
:is(& > *) { :is(& > *) {
flex: 1; flex: 1;
} }
@ -680,6 +684,12 @@ nav {
color: var(--color-gray-50); color: var(--color-gray-50);
} }
} }
nav > a {
display: flex;
align-items: center;
justify-content: center;
padding-top: calc(var(--spacing) * 1);
}
code { code {
speak-as: literal-punctuation; speak-as: literal-punctuation;
display: inline-block; display: inline-block;
@ -688,10 +698,9 @@ code {
} }
pre { pre {
margin-bottom: calc(var(--spacing) * 4); margin-bottom: calc(var(--spacing) * 4);
border-radius: var(--radius-lg);
border-style: var(--tw-border-style); border-style: var(--tw-border-style);
border-width: 1px; border-width: 1px;
border-color: var(--color-gray-300); border-color: var(--color-gray-600);
background-color: var(--color-gray-950); background-color: var(--color-gray-950);
padding: calc(var(--spacing) * 1); padding: calc(var(--spacing) * 1);
} }
@ -859,6 +868,23 @@ button {
color: var(--color-gray-950); color: var(--color-gray-950);
} }
} }
blockquote {
border-left-style: var(--tw-border-style);
border-left-width: 5px;
border-color: var(--color-gray-800);
background-color: var(--color-gray-200);
padding: calc(var(--spacing) * 2);
color: var(--color-black);
@media (prefers-color-scheme: dark) {
border-color: var(--color-black);
}
@media (prefers-color-scheme: dark) {
background-color: var(--color-gray-700);
}
@media (prefers-color-scheme: dark) {
color: var(--color-white);
}
}
shape { shape {
display: flex; display: flex;
aspect-ratio: 1 / 1; aspect-ratio: 1 / 1;
@ -911,7 +937,6 @@ shape.video {
width: 100%; width: 100%;
} }
block { block {
margin-bottom: calc(var(--spacing) * 10);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: calc(var(--spacing) * 4); gap: calc(var(--spacing) * 4);

6
tools/pragmas.sql Normal file
View file

@ -0,0 +1,6 @@
PRAGMA foreign_keys = ON;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
PRAGMA mmap_size = 134217728; -- 128 megabytes
PRAGMA journal_size_limig = 67108864; -- 64 megabytes
PRAGMA cache_size = 2000;

View file

@ -1,13 +1,12 @@
<script>
let Data = new GetJson("/api/admin/new/table/{{ .Table }}");
</script>
<h1><a href="/admin/table/{{ .Table }}/">&laquo;</a>Admin {{ .Table }}</h1> <h1><a href="/admin/table/{{ .Table }}/">&laquo;</a>Admin {{ .Table }}</h1>
<block x-data="Data"> <block x-data="{item: {}}"
x-init="item = await GetJson('/api/admin/new/table/{{ .Table }}')">
<form method="POST" action="/api/admin/new/table/{{ .Table }}"> <form method="POST" action="/api/admin/new/table/{{ .Table }}">
<card> <card>
<top><h2>New {{ .Table }}</h2></top> <top><h2>New {{ .Table }}</h2></top>
<middle> <middle>
<template x-for="(value, key) in item"> <template x-for="(value, key) in item">
<div> <div>
@ -16,10 +15,11 @@
</div> </div>
</template> </template>
</middle> </middle>
<bottom> <bottom>
<button type="button"><a href="/admin/table/{{ .Table }}/">Back</a></button> <button type="button"><a href="/admin/table/{{ .Table }}/">Back</a></button>
<button class="hover:btn-alert" type="button">Clear</button> <button class="hover:btn-alert" type="button">Clear</button>
<button class="hover:btn-hover" type="submit">Insert</button> <button class="hover:btn-hover" type="submit">Insert</button>
</bottom> </bottom>
</card> </card>
</form> </form>

View file

@ -1,10 +1,8 @@
<script>
let Data = new GetJson("/api/admin/table/{{ .Table }}/{{ .Id }}");
</script>
<h1><a href="/admin/table/{{ .Table }}/">&laquo;</a>Admin {{ .Table }}</h1> <h1><a href="/admin/table/{{ .Table }}/">&laquo;</a>Admin {{ .Table }}</h1>
<block x-data="Data"> <block x-data="{item: {}}"
x-init="item = await GetJson('/api/admin/table/{{ .Table }}/{{ .Id }}')">
<form method="POST" action="/api/admin/table/{{ .Table }}/{{ .Id }}"> <form method="POST" action="/api/admin/table/{{ .Table }}/{{ .Id }}">
<card> <card>
<top><h1>{{ .Table }} : {{ .Id }}</h1></top> <top><h1>{{ .Table }} : {{ .Id }}</h1></top>

View file

@ -8,30 +8,35 @@
<meta name="description" content="My Go learning project, which is a Twitch support thing." /> <meta name="description" content="My Go learning project, which is a Twitch support thing." />
<link rel="icon" href="/favicon.ico" type="image/x-icon" /> <link rel="icon" href="/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="/style.css"> <link rel="stylesheet" href="/style.css">
<!-- all alpine plugins come first to register into alpine:init event -->
<script defer src="/js/alpine-intersect.js"></script>
<!-- then alpine runs, triggers init, and tada you get no-build plugins -->
<script defer src="/js/alpine.js"></script> <script defer src="/js/alpine.js"></script>
<script src="/js/code.js"></script> <script src="/js/code.js"></script>
<title>ZedShaw.games</title> <title>ZedShaw.games</title>
</head> </head>
<body data-testid="{{.PageId}}"> <body id="top" data-testid="{{.PageId}}">
<header> <header class="p-4">
<a id="home" href="/"> <nav>
<svg xmlns="http://www.w3.org/2000/svg" <a id="home" href="/">
width="24" <svg xmlns="http://www.w3.org/2000/svg"
height="24" width="24"
viewBox="0 0 24 24"> height="24"
<use href="/icons/home.svg#home"><use> viewBox="0 0 24 24">
</svg> <use href="/icons/home.svg#home"><use>
</a> </svg>
<a id="live" href="/live/">Live</a> </a>
<a id="stream" href="/stream/">Streams</a> <a id="live" href="/live/">Live</a>
<a id="game" href="/game/">Games</a> <a id="stream" href="/stream/">Streams</a>
<a id="register" href="/register/">Register</a> <a id="game" href="/game/">Games</a>
<a id="login" href="/login/">Login</a> <a id="register" href="/register/">Register</a>
<a id="login" href="/login/">Login</a>
</nav>
</header> </header>
<div class="p-0 min-h-screen dark:bg-gray-900"> <main>
{{embed}} {{embed}}
</div> </main>
<footer> <footer>
<div class="flex-1"> <div class="flex-1">