This commit is contained in:
2024-09-12 08:57:44 +10:00
commit eb10ca9ca3
35 changed files with 1354 additions and 0 deletions

34
Dockerfile Normal file
View File

@@ -0,0 +1,34 @@
## Build
FROM golang:1.22-alpine AS build
ARG VERSION='dev'
RUN apk update && apk add --no-cache curl
RUN curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 \
&& chmod +x tailwindcss-linux-x64 \
&& mv tailwindcss-linux-x64 /usr/local/bin/tailwindcss
RUN go install github.com/a-h/templ/cmd/templ@v0.2.663 \
&& go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest
WORKDIR /app
COPY ./ /app
RUN templ generate -path ./components \
&& tailwindcss -i ./styles/input.css -o ./dist/assets/css/output@${VERSION}.css --minify \
&& sqlc generate
RUN go build -ldflags="-s -w -X version.Value=${VERSION}" -o my-app
## Deploy
FROM gcr.io/distroless/static-debian12
WORKDIR /
COPY --from=build /app/my-app /my-app
EXPOSE 8080
CMD ["/my-app"]

21
Makefile Normal file
View File

@@ -0,0 +1,21 @@
format-templ:
@echo "Formatting templ files..."
@templ fmt .
generate-templ:
@echo "Generating templ files..."
@templ generate -path ./components
generate-templ-watch:
@echo "Generating templ files..."
@templ generate -path ./components -watch
generate-tailwind:
@echo "Generating tailwind files..."
@tailwindcss -i ./styles/input.css -o ./dist/assets/css/output@dev.css
generate-tailwind-watch:
@echo "Generating tailwind files..."
@tailwindcss -i ./styles/input.css -o ./dist/assets/css/output@dev.css --watch
run:
@echo "Running..."
@go run main.go
build:
@echo "Building..."
@go build -o ./app -ldflags="-s -w -X version.Value=1.0.0"

242
README.md Normal file
View File

@@ -0,0 +1,242 @@
# Go + HTMX Template
This is a template repository that comes with everything you need to build a Web Application using Go (templ) and HTMX.
The template comes with a basic structure of using a SQL DB (`sqlc`), E2E testing (playwright), and styling (tailwindcss).
## Getting Started
In the top right, select the dropdown __Use this template__ and select __Create a new repository__.
Once cloned, run the `update_module.sh` script to change the module to your module name.
```shell
./update_module vctpule
```
## Technologies
A few different technologies are configured to help getting off the ground easier.
- [sqlc](https://sqlc.dev/) for database layer
- Stubbed to use SQLite
- This can be easily swapped with [sqlx](https://jmoiron.github.io/sqlx/)
- The script `upgrade_sqlc.sh` is available to upgrade GitHub Workflow files to latest sqlc version
- [Tailwind CSS](https://tailwindcss.com/) for styling
- Output is generated with the [CLI](https://tailwindcss.com/docs/installation)
- [templ](https://templ.guide/) for creating HTML
- The script `upgrade_templ.sh` is available to make upgrading easier
- [HTMX](https://htmx.org/) for HTML interaction
- The script `upgrade_htmx.sh` is available to make upgrading easier
- [air](https://github.com/cosmtrek/air) for live reloading of the application.
- [golang migrate](https://github.com/golang-migrate/migrate) for DB migrations.
- [playwright-go](https://github.com/playwright-community/playwright-go) for E2E testing.
Everything else uses the standard library.
## Structure
```text
.
├── Makefile
├── components
│   ├── core
│   │   └── html.templ
│   └── home
│   └── home.templ
├── db
│   ├── db.go
│   ├── local.go
│   ├── migrations
│   │   ├── 20240407203525_init.down.sql
│   │   └── 20240407203525_init.up.sql
│   └── queries
│   └── query.sql
├── db.sqlite3
├── dist
│   ├── assets
│   │   └── js
│   │   └── htmx@1.9.10.min.js
│   └── dist.go
├── e2e
│   ├── e2e_test.go
│   ├── home_test.go
│   └── testdata
│   └── seed.sql
├── go.mod
├── go.sum
├── log
│   └── log.go
├── main.go
├── server
│   ├── handler
│   │   ├── handler.go
│   │   └── home.go
│   ├── middleware
│   │   ├── cache.go
│   │   ├── logging.go
│   │   └── middleware.go
│   ├── router
│   │   └── router.go
│   └── server.go
├── sqlc.yml
├── styles
│   └── input.css
├── tailwind.config.js
└── version
└── version.go
```
### Components
This is where `templ` files live. Anything you want to render to the user goes here. Note, all
`*.go` files will be ignored by `git` (configured in `.gitignore`).
### DB
This is the directory that `sqlc` generates to. Update `queries.sql` to build
your database operations.
This project uses [golang migrate](https://github.com/golang-migrate/migrate) for DB
migrations. `sqlc` uses the `db/migrations` directory to generating DB tables. Call
`db.Migrate(..)` to automatically migrate your database to the latest version. To add migration
call the following command,
```shell
migrate create -ext sql -dir db/migrations <name of migration>
```
This package can be easily update to use `sqlx` as well.
### Dist
This is where your assets live. Any Javascript, images, or styling needs to go in the
`dist/assets` directory. The directory will be embedded into the application.
Note, the `dist/assets/css` will be ignored by `git` (configured in `.gitignore`) since the
files that are written to this directory are done by the Tailwind CSS CLI. Custom styles should
go in the `styles/input.css` file.
### E2E
To test the UI, the `e2e` directory contains the Go tests for performing End to end testing. To
run the tests, run the command
```shell
go test -v ./... -tags=e2e
```
The end to end tests, will start up the app, on a random port, seeding the database using the
`seed.sql` file. Once the tests are complete, the app will be stopped.
The E2E tests use Playwright (Go) for better integration into the Go tooling.
### Log
This contains helper function to create a `slog.Logger`. Log level and output type can be set
with then environment variables `LOG_LEVEL` and `LOG_OUTPUT`. The logger will write to
`stdout`.
### Server
This contains everything related to the HTTP server. It comes with a graceful shutdown handler
that handles `SIGINT`.
#### Router
This package sets up the routing for the application, such as the `/assets/` path and `/` path.
It uses the standard libraries mux for routing. You can easily swap out for other HTTP
routers such as [gorilla/mux](https://github.com/gorilla/mux).
#### Middleware
This package contains any middleware to configured with routes.
#### Handler
This package contains the handler to handle the actual routes.
#### Styles
This contains the `input.css` that the Tailwind CSS CLI uses to generate your output CSS.
Update `input.css` with any custom CSS you need and it will be included in the output CSS.
#### Version
This package allows you to set a version at build time. If not set, the version defaults to
`dev`. To set the version run the following command,
```shell
go build -o ./app -ldflags="-X version.Value=1.0.0"
```
See the `Makefile` for building the application.
## Run
There are a couple builtin ways to run the application - using `air` or the `Makefile` helper
commands.
### Prerequisites
- Install [templ](https://templ.guide/quick-start/installation)
- Install [sqlc](https://docs.sqlc.dev/en/stable/overview/install.html)
- Install [tailwindcss CLI](https://tailwindcss.com/docs/installation)
- Install [air](https://github.com/cosmtrek/air#installation)
### air
`air` has been configured with the file `.air.toml` to allow live reloading of the application
when a file changes.
To run, install `air`
```shell
go install github.com/cosmtrek/air@latest
```
Then simply run the command
```shell
air
```
#### Address Already In Use Error
Sometimes, you may run into the issue _address already in use_. If this is the case, you
can run this command to find the PID to kill it.
```shell
ps aux | grep tmp/main
```
### Makefile
You can also run with the provided `Makefile`. There are commands to generate `templ` files and
tailwind output css.
```shell
# Generate and watch templ
make generate-templ-watch
# Genrate and watch tailwindcss
make generate-tailwind-watch
# Run application
make run
```
## Github Workflow
The repository comes with two Github workflows as well. One called `ci.yml` that lints and
tests your code. The other called `release.yml` that creates a tag, GitHub Release, and
attaches the Linux binary to the Release.
Note, the version of `github.com/a-h/templ/cmd/templ` matches the version in `go.mod`. If these
do not match, the build will fail. When upgrading your `templ` version, make sure to update
`ci.yml` and `release.yml`.
### GoReleaser
If you need to compile for more than Linux, see [GoReleaser](https://goreleaser.com/) for a
better release process.

View File

@@ -0,0 +1,30 @@
package core
import "vctp/version"
templ HTML(title string, content templ.Component) {
<!DOCTYPE html>
<html lang="en">
@head(title)
@body(content)
</html>
}
templ head(title string) {
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="description" content="Hello world"/>
<title>{ title }</title>
<script src="/assets/js/htmx@v2.0.2.min.js"></script>
<link href={ "/assets/css/output@" + version.Value + ".css" } rel="stylesheet"/>
</head>
}
templ body(content templ.Component) {
<body class="flex flex-col min-h-screen">
<main class="flex-grow">
@content
</main>
</body>
}

View File

@@ -0,0 +1,8 @@
package home
templ Home() {
<div class="text-center">
<h1 class="text-5xl font-bold">Welcome!</h1>
<p class="text-indigo-200 mt-4">This is a simple home screen.</p>
</div>
}

55
db/db.go Normal file
View File

@@ -0,0 +1,55 @@
package db
import (
"database/sql"
"embed"
"fmt"
"vctp/db/queries"
"log/slog"
"github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/sqlite3"
"github.com/golang-migrate/migrate/v4/source/iofs"
)
//go:embed migrations/*.sql
var migrations embed.FS
type Database interface {
DB() *sql.DB
Queries() *queries.Queries
Logger() *slog.Logger
Close() error
}
func New(logger *slog.Logger, url string) (Database, error) {
db, err := newLocalDB(logger, url)
if err != nil {
return nil, err
}
if err = db.db.Ping(); err != nil {
return nil, err
}
return db, nil
}
// Migrate runs the migrations on the database. Assumes the database is SQLite.
func Migrate(db Database) error {
driver, err := sqlite3.WithInstance(db.DB(), &sqlite3.Config{})
if err != nil {
return fmt.Errorf("failed to create database driver: %w", err)
}
iofsDriver, err := iofs.New(migrations, "migrations")
if err != nil {
return fmt.Errorf("failed to create iofs: %w", err)
}
defer iofsDriver.Close()
m, err := migrate.NewWithInstance("iofs", iofsDriver, "sqlite3", driver)
if err != nil {
return fmt.Errorf("failed to create migration: %w", err)
}
return m.Up()
}

42
db/local.go Normal file
View File

@@ -0,0 +1,42 @@
package db
import (
"database/sql"
"vctp/db/queries"
"log/slog"
_ "github.com/tursodatabase/libsql-client-go/libsql"
_ "modernc.org/sqlite"
)
type LocalDB struct {
logger *slog.Logger
db *sql.DB
queries *queries.Queries
}
var _ Database = (*LocalDB)(nil)
func (d *LocalDB) DB() *sql.DB {
return d.db
}
func (d *LocalDB) Queries() *queries.Queries {
return d.queries
}
func (d *LocalDB) Logger() *slog.Logger {
return d.logger
}
func (d *LocalDB) Close() error {
return d.db.Close()
}
func newLocalDB(logger *slog.Logger, path string) (*LocalDB, error) {
db, err := sql.Open("libsql", "file:"+path)
if err != nil {
return nil, err
}
return &LocalDB{logger: logger, db: db, queries: queries.New(db)}, nil
}

View File

@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS authors (
id INTEGER PRIMARY KEY,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
name TEXT NOT NULL,
bio TEXT
);

View File

25
db/queries/query.sql Normal file
View File

@@ -0,0 +1,25 @@
-- name: GetAuthor :one
SELECT * FROM authors
WHERE id = ? LIMIT 1;
-- name: ListAuthors :many
SELECT * FROM authors
ORDER BY name;
-- name: CreateAuthor :one
INSERT INTO authors (
name, bio
) VALUES (
?, ?
)
RETURNING *;
-- name: UpdateAuthor :exec
UPDATE authors
SET name = ?,
bio = ?
WHERE id = ?;
-- name: DeleteAuthor :exec
DELETE FROM authors
WHERE id = ?;

1
dist/assets/js/htmx@v2.0.2.min.js vendored Normal file

File diff suppressed because one or more lines are too long

8
dist/dist.go vendored Normal file
View File

@@ -0,0 +1,8 @@
package dist
import (
"embed"
)
//go:embed all:assets
var AssetsDir embed.FS

237
e2e/e2e_test.go Normal file
View File

@@ -0,0 +1,237 @@
//go:build e2e
package e2e_test
import (
"bufio"
"database/sql"
"fmt"
"log"
"math/rand"
"net/url"
"os"
"os/exec"
"syscall"
"testing"
"time"
"github.com/playwright-community/playwright-go"
_ "github.com/tursodatabase/libsql-client-go/libsql"
_ "modernc.org/sqlite"
)
// global variables, can be used in any tests
var (
pw *playwright.Playwright
browser playwright.Browser
context playwright.BrowserContext
page playwright.Page
expect playwright.PlaywrightAssertions
isChromium bool
isFirefox bool
isWebKit bool
browserName = getBrowserName()
browserType playwright.BrowserType
app *exec.Cmd
baseUrL *url.URL
)
// defaultContextOptions for most tests
var defaultContextOptions = playwright.BrowserNewContextOptions{
AcceptDownloads: playwright.Bool(true),
HasTouch: playwright.Bool(true),
}
func TestMain(m *testing.M) {
beforeAll()
code := m.Run()
afterAll()
os.Exit(code)
}
// beforeAll prepares the environment, including
// - start Playwright driver
// - launch browser depends on BROWSER env
// - init web-first assertions, alias as `expect`
func beforeAll() {
err := playwright.Install()
if err != nil {
log.Fatalf("could not install Playwright: %v", err)
}
pw, err = playwright.Run()
if err != nil {
log.Fatalf("could not start Playwright: %v", err)
}
if browserName == "chromium" || browserName == "" {
browserType = pw.Chromium
} else if browserName == "firefox" {
browserType = pw.Firefox
} else if browserName == "webkit" {
browserType = pw.WebKit
}
// launch browser, headless or not depending on HEADFUL env
browser, err = browserType.Launch(playwright.BrowserTypeLaunchOptions{
Headless: playwright.Bool(os.Getenv("HEADFUL") == ""),
})
if err != nil {
log.Fatalf("could not launch: %v", err)
}
// init web-first assertions with 1s timeout instead of default 5s
expect = playwright.NewPlaywrightAssertions(1000)
isChromium = browserName == "chromium" || browserName == ""
isFirefox = browserName == "firefox"
isWebKit = browserName == "webkit"
// start app
if err = startApp(); err != nil {
log.Fatalf("could not start app: %v", err)
}
time.Sleep(time.Second * 5)
if err = seedDB(); err != nil {
log.Fatalf("could not seed db: %v", err)
}
}
func startApp() error {
port := getPort()
app = exec.Command("go", "run", "main.go")
app.Dir = "../"
app.Env = append(
os.Environ(),
"DB_URL=./test-db.sqlite3",
fmt.Sprintf("PORT=%d", port),
"LOG_LEVEL=DEBUG",
)
var err error
baseUrL, err = url.Parse(fmt.Sprintf("http://localhost:%d", port))
if err != nil {
return err
}
stdout, err := app.StdoutPipe()
if err != nil {
return err
}
stderr, err := app.StderrPipe()
if err != nil {
return err
}
if err := app.Start(); err != nil {
return err
}
fmt.Printf("Started app on port %d, pid %d", port, app.Process.Pid)
stdoutchan := make(chan string)
stderrchan := make(chan string)
go func() {
defer close(stdoutchan)
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
stdoutchan <- scanner.Text()
}
}()
go func() {
defer close(stderrchan)
scanner := bufio.NewScanner(stderr)
for scanner.Scan() {
stderrchan <- scanner.Text()
}
}()
go func() {
for line := range stdoutchan {
fmt.Println("[STDOUT]", line)
}
}()
go func() {
for line := range stderrchan {
fmt.Println("[STDERR]", line)
}
}()
return nil
}
func seedDB() error {
db, err := sql.Open("libsql", "file:../test-db.sqlite3")
if err != nil {
return err
}
b, err := os.ReadFile("./testdata/seed.sql")
if err != nil {
return err
}
_, err = db.Exec(string(b))
if err != nil {
return err
}
return nil
}
func getPort() int {
randomGenerator := rand.New(rand.NewSource(time.Now().UnixNano()))
return randomGenerator.Intn(9001-3000) + 3000
}
// afterAll does cleanup, e.g. stop playwright driver
func afterAll() {
if app != nil && app.Process != nil {
if err := syscall.Kill(-app.Process.Pid, syscall.SIGKILL); err != nil {
fmt.Println(err)
}
}
if err := pw.Stop(); err != nil {
log.Fatalf("could not start Playwright: %v", err)
}
if err := os.Remove("../test-db.sqlite3"); err != nil {
log.Fatalf("could not remove test-db.sqlite3: %v", err)
}
}
// beforeEach creates a new context and page for each test,
// so each test has isolated environment. Usage:
//
// Func TestFoo(t *testing.T) {
// beforeEach(t)
// // your test code
// }
func beforeEach(t *testing.T, contextOptions ...playwright.BrowserNewContextOptions) {
t.Helper()
opt := defaultContextOptions
if len(contextOptions) == 1 {
opt = contextOptions[0]
}
context, page = newBrowserContextAndPage(t, opt)
}
func getBrowserName() string {
browserName, hasEnv := os.LookupEnv("BROWSER")
if hasEnv {
return browserName
}
return "chromium"
}
func newBrowserContextAndPage(t *testing.T, options playwright.BrowserNewContextOptions) (playwright.BrowserContext, playwright.Page) {
t.Helper()
context, err := browser.NewContext(options)
if err != nil {
t.Fatalf("could not create new context: %v", err)
}
t.Cleanup(func() {
if err := context.Close(); err != nil {
t.Errorf("could not close context: %v", err)
}
})
p, err := context.NewPage()
if err != nil {
t.Fatalf("could not create new page: %v", err)
}
return context, p
}
func getFullPath(relativePath string) string {
return baseUrL.ResolveReference(&url.URL{Path: relativePath}).String()
}

16
e2e/home_test.go Normal file
View File

@@ -0,0 +1,16 @@
//go:build e2e
package e2e_test
import (
"github.com/stretchr/testify/require"
"testing"
)
func TestHome(t *testing.T) {
beforeEach(t)
_, err := page.Goto(getFullPath(""))
require.NoError(t, err)
require.NoError(t, expect.Locator(page.GetByText("Welcome!")).ToBeVisible())
}

1
e2e/testdata/seed.sql vendored Normal file
View File

@@ -0,0 +1 @@
-- TODO: fill this what you need for E2E testing

43
go.mod Normal file
View File

@@ -0,0 +1,43 @@
module vctp
go 1.23.1
require (
github.com/a-h/templ v0.2.771
github.com/golang-migrate/migrate/v4 v4.17.1
github.com/playwright-community/playwright-go v0.4201.1
github.com/stretchr/testify v1.8.4
github.com/tursodatabase/libsql-client-go v0.0.0-20240812094001-348a4e45b535
modernc.org/sqlite v1.32.0
)
require (
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/coder/websocket v1.8.12 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/deckarep/golang-set/v2 v2.6.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
github.com/go-stack/stack v1.8.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect
golang.org/x/sys v0.24.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a // indirect
modernc.org/libc v1.59.9 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
)

116
go.sum Normal file
View File

@@ -0,0 +1,116 @@
github.com/a-h/templ v0.2.771 h1:4KH5ykNigYGGpCe0fRJ7/hzwz72k3qFqIiiLLJskbSo=
github.com/a-h/templ v0.2.771/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w=
github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw=
github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4=
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/playwright-community/playwright-go v0.4201.1 h1:fFX/02r3wrL+8NB132RcduR0lWEofxRDJEKuln+9uMQ=
github.com/playwright-community/playwright-go v0.4201.1/go.mod h1:hpEOnUo/Kgb2lv5lEY29jbW5Xgn7HaBeiE+PowRad8k=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tursodatabase/libsql-client-go v0.0.0-20240812094001-348a4e45b535 h1:iLjJLq2A5J6L9zrhyNn+fpmxFvtEpYB4XLMr0rX3epI=
github.com/tursodatabase/libsql-client-go v0.0.0-20240812094001-348a4e45b535/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.20.7 h1:skrinQsjxWfvj6nbC3ztZPJy+NuwmB3hV9zX/pthNYQ=
modernc.org/ccgo/v4 v4.20.7/go.mod h1:UOkI3JSG2zT4E2ioHlncSOZsXbuDCZLvPi3uMlZT5GY=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.5.0 h1:bJ9ChznK1L1mUtAQtxi0wi5AtAs5jQuw4PrPHO5pb6M=
modernc.org/gc/v2 v2.5.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a h1:CfbpOLEo2IwNzJdMvE8aiRbPMxoTpgAJeyePh0SmO8M=
modernc.org/gc/v3 v3.0.0-20240801135723-a856999a2e4a/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/libc v1.59.9 h1:k+nNDDakwipimgmJ1D9H466LhFeSkaPPycAs1OZiDmY=
modernc.org/libc v1.59.9/go.mod h1:EY/egGEU7Ju66eU6SBqCNYaFUDuc4npICkMWnU5EE3A=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.32.0 h1:6BM4uGza7bWypsw4fdLRsLxut6bHe4c58VeqjRgST8s=
modernc.org/sqlite v1.32.0/go.mod h1:UqoylwmTb9F+IqXERT8bW9zzOWN8qwAIcLdzeBZs4hA=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

100
log/log.go Normal file
View File

@@ -0,0 +1,100 @@
package log
import (
"log/slog"
"os"
)
// New creates a new logger with the given level and output.
func New(level Level, output Output) *slog.Logger {
var h slog.Handler
switch output {
case OutputJson:
h = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: level.ToSlog()})
case OutputText:
h = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level.ToSlog()})
default:
h = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level.ToSlog()})
}
return slog.New(h)
}
// Level represents the log level.
type Level string
// ToSlog converts the level to slog.Level.
func (l Level) ToSlog() slog.Level {
switch l {
case LevelDebug:
return slog.LevelDebug
case LevelInfo:
return slog.LevelInfo
case LevelWarn:
return slog.LevelWarn
case LevelError:
return slog.LevelError
default:
return slog.LevelInfo
}
}
const (
// LogDebug is the debug log level.
LevelDebug Level = "debug"
// LogInfo is the info log level.
LevelInfo Level = "info"
// LogWarn is the warn log level.
LevelWarn Level = "warn"
// LogError is the error log level.
LevelError Level = "error"
)
// ToLevel converts the level to Level.
func ToLevel(level string) Level {
switch level {
case "debug":
return LevelDebug
case "info":
return LevelInfo
case "warn":
return LevelWarn
case "error":
return LevelError
default:
return LevelInfo
}
}
// GetLevel returns the log level from the environment variable.
func GetLevel() Level {
level := os.Getenv("LOG_LEVEL")
return ToLevel(level)
}
// Output represents the log output.
type Output string
const (
// OutputJson is the JSON log output.
OutputJson Output = "json"
// OutputText is the text log output.
OutputText Output = "text"
)
// ToOutput converts the output to Output.
func ToOutput(output string) Output {
switch output {
case "json":
return OutputJson
case "text":
return OutputText
default:
return OutputText
}
}
// GetOutput returns the log output from the environment variable.
func GetOutput() Output {
output := os.Getenv("LOG_OUTPUT")
return ToOutput(output)
}

44
main.go Normal file
View File

@@ -0,0 +1,44 @@
package main
import (
"errors"
"vctp/db"
"vctp/log"
"vctp/server"
"vctp/server/router"
"os"
"github.com/golang-migrate/migrate/v4"
)
func main() {
logger := log.New(
log.GetLevel(),
log.GetOutput(),
)
database, err := db.New(logger, "./db.sqlite3")
if err != nil {
logger.Error("Failed to create database", "error", err)
os.Exit(1)
}
defer database.Close()
if err = db.Migrate(database); err != nil && !errors.Is(err, migrate.ErrNoChange) {
logger.Error("failed to migrate database", "error", err)
return
}
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
svr := server.New(
logger,
":"+port,
server.WithRouter(router.New(logger, database)),
)
svr.StartAndWait()
}

24
server/handler/handler.go Normal file
View File

@@ -0,0 +1,24 @@
package handler
import (
"context"
"github.com/a-h/templ"
"vctp/db"
"log/slog"
"net/http"
)
// Handler handles requests.
type Handler struct {
Logger *slog.Logger
Database db.Database
}
func (h *Handler) html(ctx context.Context, w http.ResponseWriter, status int, t templ.Component) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(status)
if err := t.Render(ctx, w); err != nil {
h.Logger.Error("Failed to render component", "error", err)
}
}

12
server/handler/home.go Normal file
View File

@@ -0,0 +1,12 @@
package handler
import (
"vctp/components/core"
"vctp/components/home"
"net/http"
)
// Home handles the home page.
func (h *Handler) Home(w http.ResponseWriter, r *http.Request) {
h.html(r.Context(), w, http.StatusOK, core.HTML("Example Site", home.Home()))
}

View File

@@ -0,0 +1,18 @@
package middleware
import (
"vctp/version"
"net/http"
)
// CacheMiddleware sets the Cache-Control header based on the version.
func CacheMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if version.Value == "dev" {
w.Header().Set("Cache-Control", "no-cache")
} else {
w.Header().Set("Cache-Control", "public, max-age=31536000")
}
next.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,34 @@
package middleware
import (
"log/slog"
"net/http"
"time"
)
// LoggingMiddleware represents a logging middleware.
type LoggingMiddleware struct {
logger *slog.Logger
handler http.Handler
}
// NewLoggingMiddleware creates a new logging middleware with the given logger and handler.
func NewLoggingMiddleware(logger *slog.Logger, handler http.Handler) *LoggingMiddleware {
return &LoggingMiddleware{
logger: logger,
handler: handler,
}
}
// ServeHTTP logs the request and calls the next handler.
func (l *LoggingMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
start := time.Now()
l.handler.ServeHTTP(w, r)
l.logger.Debug(
"Request recieved",
slog.String("method", r.Method),
slog.String("path", r.URL.Path),
slog.String("remote", r.RemoteAddr),
slog.Duration("duration", time.Since(start)),
)
}

View File

@@ -0,0 +1,22 @@
package middleware
import "net/http"
type Handler func(http.Handler) http.Handler
func Chain(handlers ...Handler) Handler {
if len(handlers) == 0 {
return defaultHandler
}
return func(next http.Handler) http.Handler {
for i := len(handlers) - 1; i >= 0; i-- {
next = handlers[i](next)
}
return next
}
}
func defaultHandler(next http.Handler) http.Handler {
return next
}

24
server/router/router.go Normal file
View File

@@ -0,0 +1,24 @@
package router
import (
"vctp/db"
"vctp/dist"
"vctp/server/handler"
"vctp/server/middleware"
"log/slog"
"net/http"
)
func New(logger *slog.Logger, database db.Database) http.Handler {
h := &handler.Handler{
Logger: logger,
Database: database,
}
mux := http.NewServeMux()
mux.Handle("/assets/", middleware.CacheMiddleware(http.FileServer(http.FS(dist.AssetsDir))))
mux.HandleFunc("/", h.Home)
return middleware.NewLoggingMiddleware(logger, mux)
}

96
server/server.go Normal file
View File

@@ -0,0 +1,96 @@
package server
import (
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"time"
)
// Server represents an HTTP server.
type Server struct {
srv *http.Server
logger *slog.Logger
}
// New creates a new server with the given logger, address and options.
func New(logger *slog.Logger, addr string, opts ...Option) *Server {
srv := &http.Server{
Addr: addr,
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
for _, opt := range opts {
opt(&Server{srv: srv})
}
return &Server{
srv: srv,
logger: logger,
}
}
// Option represents a server option.
type Option func(*Server)
// WithWriteTimeout sets the write timeout.
func WithWriteTimeout(timeout time.Duration) Option {
return func(s *Server) {
s.srv.WriteTimeout = timeout
}
}
// WithReadTimeout sets the read timeout.
func WithReadTimeout(timeout time.Duration) Option {
return func(s *Server) {
s.srv.ReadTimeout = timeout
}
}
// WithRouter sets the handler.
func WithRouter(handler http.Handler) Option {
return func(s *Server) {
s.srv.Handler = handler
}
}
// StartAndWait starts the server and waits for a signal to shut down.
func (s *Server) StartAndWait() {
s.Start()
s.GracefulShutdown()
}
// Start starts the server.
func (s *Server) Start() {
go func() {
s.logger.Info("starting server", "port", s.srv.Addr)
if err := s.srv.ListenAndServe(); err != nil {
s.logger.Warn("failed to start server", "error", err)
}
}()
}
// GracefulShutdown shuts down the server gracefully.
func (s *Server) GracefulShutdown() {
c := make(chan os.Signal, 1)
// We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
signal.Notify(c, os.Interrupt)
// Block until we receive our signal.
<-c
// Create a deadline to wait for.
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// Doesn't block if no connections, but will otherwise wait
// until the timeout deadline.
_ = s.srv.Shutdown(ctx)
// Optionally, you could run srv.Shutdown in a goroutine and block on
// <-ctx.Done() if your application should wait for other services
// to finalize based on context cancellation.
s.logger.Info("shutting down")
os.Exit(0)
}

10
sqlc.yml Normal file
View File

@@ -0,0 +1,10 @@
version: 2
sql:
- engine: sqlite
queries:
- db/queries/query.sql
schema: db/migrations
gen:
go:
package: queries
out: db/queries

3
styles/input.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

14
tailwind.config.js Normal file
View File

@@ -0,0 +1,14 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./components/**/*.templ",
],
theme: {
extend: {},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
],
}

10
update_module.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/sh
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <new module name>"
exit 1
fi
new_module=$1
find . -type d -name .git -prune -o -type f -exec sed -i '' -e "s/vctp/${new_module}/g" {} \;

22
upgrade_htmx.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/sh
if [ "$#" -ne 1 ]; then
echo "Usage: $0 <new htmx version>"
exit 1
fi
old_version=""
new_version=$1
for filename in "./dist/assets/js"/*; do
if [[ "$filename" == "./dist/assets/js/htmx"* ]]; then
old_version=$(echo "$filename" | awk -F'@' '{gsub(/\.min\.js/, "", $2); print $2}')
break
fi
done
curl -sL -o "./dist/assets/js/htmx@${new_version}.min.js" "https://github.com/bigskysoftware/htmx/releases/download/${new_version}/htmx.min.js"
sed -i '' -e "s/${old_version}/${new_version}/g" "./components/core/html.templ"
rm "./dist/assets/js/htmx@${old_version}.min.js"

13
upgrade_sqlc.sh Executable file
View File

@@ -0,0 +1,13 @@
#!/bin/sh
url=$(curl -s 'https://downloads.sqlc.dev/')
new_version=$(echo "$url" | sed -e 's/<li>/<li>\n/g' | tr -d '\n' | sed -e 's/<li>/\n<li>/g' | sed -e 's/<\/li>/<\/li>\n/g' | grep -o '<li>[^<]*<a[^>]*>[^<]*</a>[^<]*</li>' | sed -e 's/<[^>]*>//g' | tail -n 1 | awk '{$1=$1;print}')
old_version=$(grep 'sqlc-version' .github/workflows/ci.yml | awk '{print $2}' | tr -d "'" | head -n 1)
for file in ".github/workflows"/*; do
if [ -f "$file" ]; then
sed -i '' -e "s/${old_version}/${new_version}/g" "$file"
fi
done

18
upgrade_templ.sh Executable file
View File

@@ -0,0 +1,18 @@
#!/bin/sh
old_version=$(grep 'github.com/a-h/templ' go.mod | awk '{print $2}')
go install github.com/a-h/templ/cmd/templ@latest
go get -u github.com/a-h/templ
go mod tidy
new_version=$(grep 'github.com/a-h/templ' go.mod | awk '{print $2}')
sed -i '' -e "s/${old_version}/${new_version}/g" "Dockerfile"
for file in ".github/workflows"/*; do
if [ -f "$file" ]; then
sed -i '' -e "s/${old_version}/${new_version}/g" "$file"
fi
done

4
version/version.go Normal file
View File

@@ -0,0 +1,4 @@
package version
// Value is the version. This is set at build time.
var Value = "dev"