diff --git a/.air.toml b/.air.toml deleted file mode 100644 index fc7bd09..0000000 --- a/.air.toml +++ /dev/null @@ -1,51 +0,0 @@ -root = "." -testdata_dir = "testdata" -tmp_dir = "tmp" - -[build] - args_bin = [] - bin = "./tmp/webserver" - cmd = "go build -o ./tmp personal-website/cmd/webserver" - delay = 1000 - exclude_dir = ["assets", "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"] - 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 - time = false - -[misc] - clean_on_exit = false - -[proxy] - app_port = 0 - enabled = false - proxy_port = 0 - -[screen] - clear_on_rebuild = false - keep_scroll = true diff --git a/.gitignore b/.gitignore index ac50a76..acbda05 100644 --- a/.gitignore +++ b/.gitignore @@ -1,25 +1,35 @@ -/* +* !README.md +!LICENSE -# go webserver code -!cmd/ +# go !internal/ -!.air.toml !go.mod !go.sum !gomod2nix.toml -cmd/webserver/public/js/ - -# blogs -!posts/ -# odin wasm code -!client/ +# uploader +!cmd/uploader # nix code -!nix/ +!nix/* !flake.* # docker -Makefile +!Makefile + +# services +!services/**/default.nix +!services/**/*.go +!services/**/*.templ +services/**/*_templ.go + +# webserver +!.air.toml +!services/webserver/public/** +services/webserver/**/*.js +!services/webserver/public/tailwind.config.js +!posts/ + +!*/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..71b07aa --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Ethan Thoma + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile index 07d9762..7413347 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,65 @@ IMAGE_NAME = webserver TAG = 0.1 +PORT = 8080 +SYSTEM = x86_64-linux -run: build - docker load < result - docker run --rm --env-file ./.env -p 127.0.0.1:8080:8080 -t $(IMAGE_NAME):$(TAG) +run: + nix run github:Mic92/nix-fast-build -- --flake '.#packages.$(SYSTEM).default' + nix run -build: +docker/build: nix build .#container + +docker: docker/build + docker load < result + docker run \ + --rm \ + --env-file ./.env \ + -p 127.0.0.1:$(PORT):$(PORT) \ + -t $(IMAGE_NAME):$(TAG) + +live/templ: + templ generate \ + --watch \ + --proxy="http://localhost:3001" \ + --open-browser=false \ + -v \ + --path=./services/webserver/ + +live/server: + air \ + --build.cmd "go build -o tmp/bin/main personal-website/services/webserver" \ + --build.bin "tmp/bin/main" \ + --build.delay "100" \ + --build.include_dir "personal-website/services/webserver" \ + --build.include_ext "go" \ + --build.stop_on_error "false" \ + --misc.clean_on_exit true \ + --proxy.enabled true \ + --proxy.proxy_port 3001 \ + --proxy.app_port $(PORT) + +live/tailwind: + tailwindcss \ + -c ./services/webserver/public/tailwind.config.js \ + -i ./services/webserver/public/main.css \ + -o ./public/main.css \ + -m \ + -w + +live/sync_assets: + rsync -a ./services/webserver/public/* ./public --exclude='*.css' + sleep 0.1 + air \ + --build.cmd "templ generate --notify-proxy" \ + --build.bin "true" \ + --build.delay "100" \ + --build.exclude_dir "" \ + --build.include_dir "public" \ + --build.include_ext "js,css" + +live: + make live/templ & \ + make live/server & \ + make live/tailwind & \ + make live/sync_assets diff --git a/README.md b/README.md index 00b6e75..a0a7122 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

Logo
@@ -18,15 +18,34 @@ | Tech | Stack | |-------|----------| -| Go | Backend | +| GO | Backend | | Htmx | Frontend | | Turso | Database | +We use [templ](https://github.com/a-h/templ) for templating and [tailwindcss](https://github.com/tailwindlabs/tailwindcss) +for styles. So it is technically more like the GoTTTH stack... + ## Building + Running -The nix flake has three derivations: +The nix flake has four derivations: - .#default: this produces the webserver binary - .#container: docker image containing the webserver binary - .#uploader: simple CLI to upload my markdown blogs +- .#blob: a WIP blob storage service I plan to use for my images + +## Developing + +The [make file](./Makefile) in root is setup for running air w/ livereload. +It will run tailwindcss, templ, and air. + +> [!TIP] +> You can also locally deply the docker image using `make docker`. + +The webserver assumes the port is set to ":8080". This should be set in your dotenv. +The dotenv file should contain: +- TURSO_DATABASE_URL +- TURSO_AUTH_TOKEN +- WEBSERVER_PORT -You can run it test the webserver locally with docker with `make run`. +> [!NOTE] +> WEBSERVER_PORT will probably be moved to the flake config instead diff --git a/client/main.odin b/client/main.odin deleted file mode 100644 index 23acd0b..0000000 --- a/client/main.odin +++ /dev/null @@ -1,46 +0,0 @@ -//+build js -package main - -import "base:runtime" -import "vendor:wasm/js" - -HTML_Canvas_ID :: "wasm" - -Context :: struct { - initialized: bool, - accumulated_time: f64, -} - -state: Context = { - initialized = false, - accumulated_time = 1, -} - -main :: proc() {} - -run :: proc() { - state.initialized = true -} - -@(export) -step :: proc(delta_time: f64) -> (keep_going: bool) { - if !state.initialized { - return true - } - state.accumulated_time += delta_time - - if state.accumulated_time > 1 { - frame(state.accumulated_time) - } - - for state.accumulated_time > 1 { - state.accumulated_time -= 1 - } - - return true -} - -frame :: proc(delta_time: f64) {} - -@(private = "file", fini) -finish :: proc() {} diff --git a/cmd/webserver/components/ascii/ascii.css b/cmd/webserver/components/ascii/ascii.css deleted file mode 100644 index 328fef4..0000000 --- a/cmd/webserver/components/ascii/ascii.css +++ /dev/null @@ -1,14 +0,0 @@ -.ascii { - display: block; - width: 100%; - line-height: 0; - text-wrap: nowrap; - white-space-collapse: preserve; - text-align: center; - - & .ascii-char { - display: inline-block; - font-size: 5px; - width: 3px; - } -} diff --git a/cmd/webserver/components/ascii/ascii.go b/cmd/webserver/components/ascii/ascii.go deleted file mode 100644 index ea67071..0000000 --- a/cmd/webserver/components/ascii/ascii.go +++ /dev/null @@ -1,21 +0,0 @@ -package components - -import ( - "html/template" -) - -type Props struct { - Ascii [][]string -} - -func (props Props) Component(t *template.Template) error { - name := "ascii" - - filepath := "cmd/webserver/components/" + name + "/" + name + ".tmpl" - - if _, err := t.New(name + "-component").ParseFiles(filepath); err != nil { - return err - } else { - return nil - } -} diff --git a/cmd/webserver/components/ascii/ascii.tmpl b/cmd/webserver/components/ascii/ascii.tmpl deleted file mode 100644 index b24c244..0000000 --- a/cmd/webserver/components/ascii/ascii.tmpl +++ /dev/null @@ -1,7 +0,0 @@ -{{ define "ascii" }} -
- {{ range .Ascii }} - {{ range $char := . }}{{ $char }}{{ end }}
- {{ end }} -
-{{ end }} diff --git a/cmd/webserver/components/footer/footer.css b/cmd/webserver/components/footer/footer.css deleted file mode 100644 index 28b18e5..0000000 --- a/cmd/webserver/components/footer/footer.css +++ /dev/null @@ -1,10 +0,0 @@ -footer { - background-color: var(--clr-text); - color: var(--clr-base); - border-block-start: var(--space-3xs) solid var(--clr-high); - font-size: var(--fs-100); - margin-block-start: var(--space-xl); - padding-block: var(--space-s); - text-align: center; - width: 100%; -} diff --git a/cmd/webserver/components/footer/footer.go b/cmd/webserver/components/footer/footer.go deleted file mode 100644 index b05e665..0000000 --- a/cmd/webserver/components/footer/footer.go +++ /dev/null @@ -1,19 +0,0 @@ -package components - -import ( - "html/template" -) - -type Props struct{} - -func (props Props) Component(t *template.Template) error { - name := "footer" - - filepath := "cmd/webserver/components/" + name + "/" + name + ".tmpl" - - if _, err := t.New(name + "-component").ParseFiles(filepath); err != nil { - return err - } else { - return nil - } -} diff --git a/cmd/webserver/components/footer/footer.tmpl b/cmd/webserver/components/footer/footer.tmpl deleted file mode 100644 index 743bf4c..0000000 --- a/cmd/webserver/components/footer/footer.tmpl +++ /dev/null @@ -1,5 +0,0 @@ -{{ define "footer" }} - -{{ end }} diff --git a/cmd/webserver/components/header/header.css b/cmd/webserver/components/header/header.css deleted file mode 100644 index d9b1c3b..0000000 --- a/cmd/webserver/components/header/header.css +++ /dev/null @@ -1,38 +0,0 @@ -header { - align-items: center; - display: flex; - flex-direction: column; - margin-block-end: var(--space-s-l); - padding-block: var(--space-xs-s); - - & nav { - width: 100%; - - & a.selected { - font-weight: 700; - text-decoration: underline; - } - - & ul { - display: flex; - justify-content: space-evenly; - list-style-type: none; - text-align: center; - - & li { - flex: 1; - - & a { - display: block; - padding-block: var(--space-2xs); - padding-inline: var(--space-xs); - } - } - } - } - - .divider { - border-block-start: var(--space-3xs) solid var(--clr-high); - width: 100%; - } -} diff --git a/cmd/webserver/components/header/header.go b/cmd/webserver/components/header/header.go deleted file mode 100644 index c2f3b2e..0000000 --- a/cmd/webserver/components/header/header.go +++ /dev/null @@ -1,39 +0,0 @@ -package components - -import ( - "html/template" - - ascii "personal-website/cmd/webserver/components/ascii" - nav "personal-website/cmd/webserver/components/nav" -) - -type Props struct { - Ascii [][]string - PageCurrent string - Pages []string -} - -func (props Props) Component(t *template.Template) error { - name := "header" - - filepath := "cmd/webserver/components/" + name + "/" + name + ".tmpl" - - if err := (ascii.Props{ - Ascii: props.Ascii, - }.Component(t)); err != nil { - return err - } - - if err := (nav.Props{ - PageCurrent: props.PageCurrent, - Pages: props.Pages, - }.Component(t)); err != nil { - return err - } - - if _, err := t.New(name + "-component").ParseFiles(filepath); err != nil { - return err - } else { - return nil - } -} diff --git a/cmd/webserver/components/header/header.tmpl b/cmd/webserver/components/header/header.tmpl deleted file mode 100644 index eedc4d6..0000000 --- a/cmd/webserver/components/header/header.tmpl +++ /dev/null @@ -1,11 +0,0 @@ -{{ define "header" }} -
- {{ template "ascii" . }} - {{ template "nav" . }} -
-
-{{ end }} - -{{ define "header-oob" }} - {{ template "nav-oob" . }} -{{ end }} diff --git a/cmd/webserver/components/index.css b/cmd/webserver/components/index.css deleted file mode 100644 index 050adcf..0000000 --- a/cmd/webserver/components/index.css +++ /dev/null @@ -1,5 +0,0 @@ -@import "./ascii/ascii.css"; -@import "./footer/footer.css"; -@import "./header/header.css"; -@import "./nav/nav.css"; -@import "./spacer/spacer.css"; diff --git a/cmd/webserver/components/nav/nav.css b/cmd/webserver/components/nav/nav.css deleted file mode 100644 index f908eec..0000000 --- a/cmd/webserver/components/nav/nav.css +++ /dev/null @@ -1,25 +0,0 @@ -#nav { - width: 100%; - - & a.selected { - font-weight: 700; - text-decoration: underline; - } - - & ul { - display: flex; - justify-content: space-evenly; - list-style-type: none; - text-align: center; - - & li { - flex: 1; - - & a { - display: block; - padding-block: var(--space-2xs); - padding-inline: var(--space-xs); - } - } - } -} diff --git a/cmd/webserver/components/nav/nav.go b/cmd/webserver/components/nav/nav.go deleted file mode 100644 index 56afe3c..0000000 --- a/cmd/webserver/components/nav/nav.go +++ /dev/null @@ -1,22 +0,0 @@ -package components - -import ( - "html/template" -) - -type Props struct { - PageCurrent string - Pages []string -} - -func (props Props) Component(t *template.Template) error { - name := "nav" - - filepath := "cmd/webserver/components/" + name + "/" + name + ".tmpl" - - if _, err := t.New(name + "-component").ParseFiles(filepath); err != nil { - return err - } else { - return nil - } -} diff --git a/cmd/webserver/components/nav/nav.tmpl b/cmd/webserver/components/nav/nav.tmpl deleted file mode 100644 index d26f9a0..0000000 --- a/cmd/webserver/components/nav/nav.tmpl +++ /dev/null @@ -1,24 +0,0 @@ -{{ define "nav" }} - -{{ end }} - -{{ define "nav-oob" }} - {{ template "nav" . }} -{{ end }} - -{{ define "nav-links" }} - {{ range $name := .Pages }} -
  • {{ $name }}
  • - {{ end }} -{{ end }} diff --git a/cmd/webserver/components/spacer/spacer.css b/cmd/webserver/components/spacer/spacer.css deleted file mode 100644 index 688a345..0000000 --- a/cmd/webserver/components/spacer/spacer.css +++ /dev/null @@ -1,3 +0,0 @@ -.spacer { - padding-inline: var(--space-2xs) -} diff --git a/cmd/webserver/components/spacer/spacer.go b/cmd/webserver/components/spacer/spacer.go deleted file mode 100644 index 71cf87b..0000000 --- a/cmd/webserver/components/spacer/spacer.go +++ /dev/null @@ -1,19 +0,0 @@ -package components - -import ( - "html/template" -) - -type Props struct{} - -func (props Props) Component(t *template.Template) error { - name := "spacer" - - filepath := "cmd/webserver/components/" + name + "/" + name + ".tmpl" - - if _, err := t.New(name + "-component").ParseFiles(filepath); err != nil { - return err - } else { - return nil - } -} diff --git a/cmd/webserver/components/spacer/spacer.tmpl b/cmd/webserver/components/spacer/spacer.tmpl deleted file mode 100644 index 4d3e96a..0000000 --- a/cmd/webserver/components/spacer/spacer.tmpl +++ /dev/null @@ -1,3 +0,0 @@ -{{ define "spacer" }} - - -{{ end }} diff --git a/cmd/webserver/layouts/base/base.css b/cmd/webserver/layouts/base/base.css deleted file mode 100644 index 859c6bf..0000000 --- a/cmd/webserver/layouts/base/base.css +++ /dev/null @@ -1,103 +0,0 @@ -body { - align-items: center; - background-color: var(--clr-base); - color: var(--clr-text); - display: flex; - flex-direction: column; - font-family: Arial, sans-serif; - font-size: var(--fs-300); - line-height: 1.5; - max-width: 100svw; - overflow-x: hidden; - - --max-width: 640px; - --min-width: 320px; - - & main { - flex: 1 1 auto; - } - - & main, - & header { - display: block; - width: clamp(var(--min-width), 75%, var(--max-width)); - margin-inline: auto; - padding-inline: var(--space-2xs-s); - } -} - -h1, -h2, -h3 { - font-family: var(--ff-heading); - font-weight: 600; - letter-spacing: -0.01em; -} - -h1 { - font-size: var(--fs-600); -} - -h2 { - font-size: var(--fs-500); -} - -h3 { - font-size: var(--fs-400); -} - -a { - color: var(--clr-link); -} - -.content { - &>*+* { - margin-block-start: var(--space-2xs-xs); - } - - & h2, - & h3 { - margin-block-start: var(--space-l-xl); - } - - & pre { - background: --var(--clr-high); - font-size: var(--fs-200); - overflow-x: auto; - padding: var(--space-xs); - - &>code { - word-wrap: break-word; - } - } - - & ul, - & ol { - list-style-position: inside; - - &>li { - margin-inline-start: var(--space-m); - } - - &>*+li { - margin-block-start: var(--space-3xs); - } - } - - & blockquote { - border-inline-start: var(--space-3xs) solid var(--clr-high); - padding-inline-start: var(--space-3xs); - } -} - -.post { - display: flex; - - & a { - align-self: center; - - & h2 { - font-size: var(--fs-300); - } - } -} diff --git a/cmd/webserver/layouts/base/base.go b/cmd/webserver/layouts/base/base.go deleted file mode 100644 index 1697a94..0000000 --- a/cmd/webserver/layouts/base/base.go +++ /dev/null @@ -1,40 +0,0 @@ -package base - -import ( - "html/template" - - // Components - footer "personal-website/cmd/webserver/components/footer" - header "personal-website/cmd/webserver/components/header" -) - -type Props struct { - Ascii [][]string - Pages []string - PageCurrent string -} - -func (props Props) Layout(t *template.Template) error { - name := "base" - - filepath := "cmd/webserver/layouts/" + name + "/" + name + ".tmpl" - - if _, err := t.New(name + "-layout").ParseFiles(filepath); err != nil { - return err - } - - // Components - if err := (footer.Props{}.Component(t)); err != nil { - return err - } - - if err := (header.Props{ - Ascii: props.Ascii, - PageCurrent: props.PageCurrent, - Pages: props.Pages, - }.Component(t)); err != nil { - return err - } - - return nil -} diff --git a/cmd/webserver/layouts/base/base.tmpl b/cmd/webserver/layouts/base/base.tmpl deleted file mode 100644 index 003c712..0000000 --- a/cmd/webserver/layouts/base/base.tmpl +++ /dev/null @@ -1,26 +0,0 @@ -{{ define "base-layout" }} - - - - - - - - - - - - - - - - - - {{ template "header" . }} -
    - {{ template "content" . }} -
    - {{ template "footer" . }} - - -{{ end }} diff --git a/cmd/webserver/layouts/index.css b/cmd/webserver/layouts/index.css deleted file mode 100644 index d0489a6..0000000 --- a/cmd/webserver/layouts/index.css +++ /dev/null @@ -1 +0,0 @@ -@import "./base/base.css"; diff --git a/cmd/webserver/main.css b/cmd/webserver/main.css deleted file mode 100644 index 393099d..0000000 --- a/cmd/webserver/main.css +++ /dev/null @@ -1,8 +0,0 @@ -@layer core, layouts, pages, components; - -@import "./public/reset.css" layer(core); -@import "./public/variables.css" layer(core); - -@import "./layouts/index.css" layer(layouts); -@import "./pages/index.css" layer(pages); -@import "./components/index.css" layer(components); diff --git a/cmd/webserver/main.go b/cmd/webserver/main.go deleted file mode 100644 index 17e59d2..0000000 --- a/cmd/webserver/main.go +++ /dev/null @@ -1,104 +0,0 @@ -package main - -import ( - "log" - "net/http" - "os" - "path/filepath" - "strings" - - "personal-website/cmd/webserver/cache" - - // pages - "personal-website/cmd/webserver/pages/blog" - "personal-website/cmd/webserver/pages/home" - "personal-website/cmd/webserver/pages/post" - "personal-website/cmd/webserver/pages/projects" -) - -var logRequests = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - log.Printf("%s %s\n", req.Method, req.URL) - http.DefaultServeMux.ServeHTTP(w, req) -}) - -func main() { - log.Println("Starting server...") - - cache.InitCache() - - http.HandleFunc("GET /healthy", func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }) - - http.HandleFunc("GET /robots.txt", staticHandler(http.Dir("static/seo"))) - - // pages - - ascii := createAscii() - - pageHome := &home.Props{Pages: []string{"home", "blog", "projects"}, Ascii: ascii} - http.Handle("GET /", pageHome) - http.Handle("GET /home", pageHome) - http.Handle("GET /home/content", pageHome) - - pageBlog := &blog.Props{Pages: []string{"home", "blog", "projects"}, Ascii: ascii} - http.Handle("GET /blog", pageBlog) - http.Handle("GET /blog/content", pageBlog) - - pagePost := &post.Props{Pages: []string{"home", "blog", "projects"}, Ascii: ascii} - http.Handle("GET /post/{slug}", pagePost) - http.Handle("GET /post/{slug}/content", pagePost) - - pageProjects := &projects.Props{Pages: []string{"home", "blog", "projects"}, Ascii: ascii} - http.Handle("GET /projects", pageProjects) - http.Handle("GET /projects/content", pageProjects) - - // static - - http.Handle("GET /public/", http.StripPrefix("/public/", staticHandler(http.Dir("cmd/webserver/public")))) - http.Handle("GET /main.css", staticHandler(http.Dir("cmd/webserver/"))) - http.Handle("GET /layouts/", http.StripPrefix("/layouts/", staticHandler(http.Dir("cmd/webserver/layouts")))) - http.Handle("GET /pages/", http.StripPrefix("/pages/", staticHandler(http.Dir("cmd/webserver/pages")))) - http.Handle("GET /components/", http.StripPrefix("/components/", staticHandler(http.Dir("cmd/webserver/components")))) - - log.Fatal(http.ListenAndServe(":8080", logRequests)) -} - -func createAscii() [][]string { - fileBuffer, err := os.ReadFile("cmd/webserver/public/images/ascii.txt") - if err != nil { - log.Printf("main: reading ascii (%v)", err) - } - - lines := strings.Split(string(fileBuffer), "\n") - - asciiChars := make([][]string, len(lines)) - - for i, line := range lines { - chars := strings.Split(line, "") - asciiChars[i] = chars - } - - return asciiChars -} - -func staticHandler(root http.FileSystem) http.HandlerFunc { - fileServer := http.FileServer(root) - - return func(w http.ResponseWriter, r *http.Request) { - ext := strings.ToLower(filepath.Ext(r.URL.Path)) - - switch ext { - case ".css": - w.Header().Set("Cache-Control", "public, max-age=1800, immutable") - case ".js": - w.Header().Set("Cache-Control", "public, max-age=604800, immutable") - case ".jpg", ".jpeg", ".png", ".gif", ".ico": - w.Header().Set("Cache-Control", "public, max-age=2592000, immutable") - default: - w.Header().Set("Cache-Control", "max-age=0") - } - - fileServer.ServeHTTP(w, r) - } -} diff --git a/cmd/webserver/pages/blog/blog.css b/cmd/webserver/pages/blog/blog.css deleted file mode 100644 index e69de29..0000000 diff --git a/cmd/webserver/pages/blog/blog.go b/cmd/webserver/pages/blog/blog.go deleted file mode 100755 index 59e7eed..0000000 --- a/cmd/webserver/pages/blog/blog.go +++ /dev/null @@ -1,86 +0,0 @@ -package blog - -import ( - "html/template" - "log" - "net/http" - "strings" - "time" - - "personal-website/cmd/webserver/layouts/base" - - spacer "personal-website/cmd/webserver/components/spacer" - - "personal-website/cmd/webserver/cache" - "personal-website/internal" -) - -type Props struct { - Ascii [][]string - PageCurrent string - Pages []string - Posts []internal.Post -} - -func (p *Props) ServeHTTP(w http.ResponseWriter, r *http.Request) { - name := "blog" - - // Page props - p.PageCurrent = name - - posts, err := cache.Cache.GetPosts() - if err != nil { - log.Printf(name+": failed to get posts (%v)", err) - } - p.Posts = posts - - // Page Template - t, err := template.New("").Funcs(template.FuncMap{ - "formatDate": func(date time.Time) string { - return date.Format("20060102") - }, - }).ParseFiles("cmd/webserver/pages/" + name + "/" + name + ".tmpl") - if err != nil { - log.Printf(name+": failed to parse tmpl file (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // Layout - if err = (base. - Props{ - Ascii: p.Ascii, - PageCurrent: p.PageCurrent, - Pages: p.Pages, - }.Layout(t)); err != nil { - log.Printf(name+": failed to render layout (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // Components - if err = (spacer.Props{}).Component(t); err != nil { - log.Printf(name+": failed to render spacer (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // Render - if strings.HasSuffix(r.URL.Path, "/content") { - if err = t.ExecuteTemplate(w, "content", p); err != nil { - log.Printf(name+": failed to render content (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - if err = t.ExecuteTemplate(w, "oob", p); err != nil { - log.Printf(name+": failed to render oob (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - } else if err = t.ExecuteTemplate(w, "page", p); err != nil { - log.Printf(name+": failed to render page (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} diff --git a/cmd/webserver/pages/blog/blog.tmpl b/cmd/webserver/pages/blog/blog.tmpl deleted file mode 100644 index e8b28bb..0000000 --- a/cmd/webserver/pages/blog/blog.tmpl +++ /dev/null @@ -1,26 +0,0 @@ -{{ define "page" }} - {{ template "base-layout" . }} -{{ end }} - -{{ define "content" }} - Ethan Thoma \ Blog -

    Blog

    - -{{ end }} - -{{ define "oob" }} - {{ template "header-oob" . }} -{{ end }} diff --git a/cmd/webserver/pages/home/home.css b/cmd/webserver/pages/home/home.css deleted file mode 100644 index e69de29..0000000 diff --git a/cmd/webserver/pages/home/home.go b/cmd/webserver/pages/home/home.go deleted file mode 100755 index 52b5494..0000000 --- a/cmd/webserver/pages/home/home.go +++ /dev/null @@ -1,66 +0,0 @@ -package home - -import ( - "html/template" - "log" - "net/http" - "strings" - "time" - - "personal-website/cmd/webserver/layouts/base" -) - -type Props struct { - Ascii [][]string - PageCurrent string - Pages []string -} - -func (p *Props) ServeHTTP(w http.ResponseWriter, r *http.Request) { - name := "home" - - // Page props - p.PageCurrent = name - - // Page Template - t, err := template.New("").Funcs(template.FuncMap{ - "formatDate": func(date time.Time) string { - return date.Format("20060102") - }, - }).ParseFiles("cmd/webserver/pages/" + name + "/" + name + ".tmpl") - if err != nil { - log.Printf(name+": failed to parse tmpl file (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // Layout - if err = (base.Props{ - Ascii: p.Ascii, - PageCurrent: p.PageCurrent, - Pages: p.Pages, - }.Layout(t)); err != nil { - log.Printf(name+": failed to render layout (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // Render - if strings.HasSuffix(r.URL.Path, "/content") { - if err = t.ExecuteTemplate(w, "content", p); err != nil { - log.Printf(name+": failed to render content (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - if err = t.ExecuteTemplate(w, "oob", p); err != nil { - log.Printf(name+": failed to render oob (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - } else if err = t.ExecuteTemplate(w, "page", p); err != nil { - log.Printf(name+": failed to render page (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} diff --git a/cmd/webserver/pages/home/home.tmpl b/cmd/webserver/pages/home/home.tmpl deleted file mode 100644 index 79c6195..0000000 --- a/cmd/webserver/pages/home/home.tmpl +++ /dev/null @@ -1,26 +0,0 @@ -{{ define "page" }} - {{ template "base-layout" . }} -{{ end }} - -{{ define "content" }} - Ethan Thoma - -
    -

    Ethan Thoma

    -

    - ML graduate student @ STASER Lab UBC -
    - Focused on NLP and RL research -

    -

    Contact:

    - -
    -{{ end }} - -{{ define "oob" }} - {{ template "header-oob" . }} -{{ end }} diff --git a/cmd/webserver/pages/index.css b/cmd/webserver/pages/index.css deleted file mode 100644 index 0e83b8a..0000000 --- a/cmd/webserver/pages/index.css +++ /dev/null @@ -1,5 +0,0 @@ -@import "./blog/blog.css"; -@import "./home/home.css"; -@import "./post/post.css"; -@import "./projects/projects.css"; -@import "./wasm/wasm.css"; diff --git a/cmd/webserver/pages/post/post.css b/cmd/webserver/pages/post/post.css deleted file mode 100644 index e69de29..0000000 diff --git a/cmd/webserver/pages/post/post.go b/cmd/webserver/pages/post/post.go deleted file mode 100755 index 08aa4b1..0000000 --- a/cmd/webserver/pages/post/post.go +++ /dev/null @@ -1,109 +0,0 @@ -package post - -import ( - "bytes" - "html/template" - "log" - "net/http" - "strings" - "time" - - "github.com/yuin/goldmark" - highlighting "github.com/yuin/goldmark-highlighting/v2" - - "personal-website/cmd/webserver/cache" - "personal-website/cmd/webserver/layouts/base" - "personal-website/internal" -) - -type Props struct { - Ascii [][]string - PageCurrent string - Pages []string - Post internal.Post -} - -func (p *Props) ServeHTTP(w http.ResponseWriter, r *http.Request) { - name := "post" - - // Page props - p.PageCurrent = "blog" - - slug := r.PathValue("slug") - post, err := getPost(slug) - if err != nil { - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - } - p.Post = post - - // Page Template - t, err := template.New("").Funcs(template.FuncMap{ - "formatDate": func(date time.Time) string { - return date.Format("20060102") - }, - }).ParseFiles("cmd/webserver/pages/" + name + "/" + name + ".tmpl") - if err != nil { - log.Printf(name+": failed to parse tmpl file (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // Layout - if err = (base.Props{ - Ascii: p.Ascii, - PageCurrent: p.PageCurrent, - Pages: p.Pages, - }.Layout(t)); err != nil { - log.Printf(name+": failed to render layout (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // Render - w.Header().Set("Cache-Control", "public, max-age=86400, immutable") - - if strings.HasSuffix(r.URL.Path, "/content") { - if err = t.ExecuteTemplate(w, "content", p); err != nil { - log.Printf(name+": failed to render content (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - if err = t.ExecuteTemplate(w, "oob", p); err != nil { - log.Printf(name+": failed to render oob (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - } else if err = t.ExecuteTemplate(w, "page", p); err != nil { - log.Printf(name+": failed to render page (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} - -func getPost(slug string) (internal.Post, error) { - post, err := cache.Cache.GetPost(slug) - if err != nil { - log.Printf("Error getting post %s: %v", slug, err) - return post, err - } - - mdRenderer := goldmark.New( - goldmark.WithExtensions( - highlighting.NewHighlighting( - highlighting.WithStyle("rose-pine"), - ), - ), - ) - - var buf bytes.Buffer - err = mdRenderer.Convert([]byte(post.Content), &buf) - if err != nil { - log.Printf("Error parsing post %s to markdown: %v", slug, err) - return post, err - } - - post.HTML = template.HTML(buf.String()) - - return post, nil -} diff --git a/cmd/webserver/pages/post/post.tmpl b/cmd/webserver/pages/post/post.tmpl deleted file mode 100644 index 8854fd2..0000000 --- a/cmd/webserver/pages/post/post.tmpl +++ /dev/null @@ -1,15 +0,0 @@ -{{ define "page" }} - {{ template "base-layout" . }} -{{ end }} - -{{ define "content" }} - Ethan Thoma \ {{ .Post.Title }} - -
    - {{ .Post.HTML }} -
    -{{ end }} - -{{ define "oob" }} - {{ template "header-oob" . }} -{{ end }} diff --git a/cmd/webserver/pages/projects/projects.css b/cmd/webserver/pages/projects/projects.css deleted file mode 100644 index e69de29..0000000 diff --git a/cmd/webserver/pages/projects/projects.go b/cmd/webserver/pages/projects/projects.go deleted file mode 100755 index 5124131..0000000 --- a/cmd/webserver/pages/projects/projects.go +++ /dev/null @@ -1,75 +0,0 @@ -package projects - -import ( - "html/template" - "log" - "net/http" - "strings" - "time" - - "personal-website/cmd/webserver/layouts/base" - - spacer "personal-website/cmd/webserver/components/spacer" -) - -type Props struct { - Ascii [][]string - PageCurrent string - Pages []string -} - -func (p *Props) ServeHTTP(w http.ResponseWriter, r *http.Request) { - name := "projects" - - // Page props - p.PageCurrent = name - - // Page Template - t, err := template.New("").Funcs(template.FuncMap{ - "formatDate": func(date time.Time) string { - return date.Format("20060102") - }, - }).ParseFiles("cmd/webserver/pages/" + name + "/" + name + ".tmpl") - if err != nil { - log.Printf(name+": failed to parse tmpl file (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // Layout - if err = (base.Props{ - Ascii: p.Ascii, - PageCurrent: p.PageCurrent, - Pages: p.Pages, - }.Layout(t)); err != nil { - log.Printf(name+": failed to render layout (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // Components - if err = (spacer.Props{}).Component(t); err != nil { - log.Printf(name+": failed to render spacer (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // Render - if strings.HasSuffix(r.URL.Path, "/content") { - if err = t.ExecuteTemplate(w, "content", p); err != nil { - log.Printf(name+": failed to render content (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - if err = t.ExecuteTemplate(w, "oob", p); err != nil { - log.Printf(name+": failed to render oob (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - } else if err = t.ExecuteTemplate(w, "page", p); err != nil { - log.Printf(name+": failed to render page (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} diff --git a/cmd/webserver/pages/projects/projects.tmpl b/cmd/webserver/pages/projects/projects.tmpl deleted file mode 100644 index f0771e8..0000000 --- a/cmd/webserver/pages/projects/projects.tmpl +++ /dev/null @@ -1,35 +0,0 @@ -{{ define "page" }} - {{ template "base-layout" . }} -{{ end }} - -{{ define "content" }} - Ethan Thoma \ Projects - - -

    Projects

    - -{{ end }} - -{{ define "oob" }} - {{ template "header-oob" . }} -{{ end }} diff --git a/cmd/webserver/pages/wasm/wasm.css b/cmd/webserver/pages/wasm/wasm.css deleted file mode 100644 index e69de29..0000000 diff --git a/cmd/webserver/pages/wasm/wasm.go b/cmd/webserver/pages/wasm/wasm.go deleted file mode 100755 index 65c310b..0000000 --- a/cmd/webserver/pages/wasm/wasm.go +++ /dev/null @@ -1,66 +0,0 @@ -package wasm - -import ( - "html/template" - "log" - "net/http" - "strings" - "time" - - "personal-website/cmd/webserver/layouts/base" -) - -type Props struct { - Ascii [][]string - PageCurrent string - Pages []string -} - -func (p *Props) ServeHTTP(w http.ResponseWriter, r *http.Request) { - name := "wasm" - - // Page props - p.PageCurrent = name - - // Page Template - t, err := template.New("").Funcs(template.FuncMap{ - "formatDate": func(date time.Time) string { - return date.Format("20060102") - }, - }).ParseFiles("cmd/webserver/pages/" + name + "/" + name + ".tmpl") - if err != nil { - log.Printf(name+": failed to parse tmpl file (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // Layout - if err = (base.Props{ - Ascii: p.Ascii, - PageCurrent: p.PageCurrent, - Pages: p.Pages, - }.Layout(t)); err != nil { - log.Printf(name+": failed to render layout (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - // Render - if strings.HasSuffix(r.URL.Path, "/content") { - if err = t.ExecuteTemplate(w, "content", p); err != nil { - log.Printf(name+": failed to render content (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - - if err = t.ExecuteTemplate(w, "oob", p); err != nil { - log.Printf(name+": failed to render oob (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } - } else if err = t.ExecuteTemplate(w, "page", p); err != nil { - log.Printf(name+": failed to render page (%v)", err) - w.WriteHeader(http.StatusInternalServerError) - return - } -} diff --git a/cmd/webserver/pages/wasm/wasm.tmpl b/cmd/webserver/pages/wasm/wasm.tmpl deleted file mode 100644 index 09a780a..0000000 --- a/cmd/webserver/pages/wasm/wasm.tmpl +++ /dev/null @@ -1,17 +0,0 @@ -{{ define "page" }} - {{ template "base-layout" . }} -{{ end }} - -{{ define "content" }} - Ethan Thoma \ Wasm - - - - -{{ end }} - -{{ define "oob" }} - {{ template "header-oob" . }} -{{ end }} diff --git a/cmd/webserver/public/images/ascii.txt b/cmd/webserver/public/images/ascii.txt deleted file mode 100644 index a4cd987..0000000 --- a/cmd/webserver/public/images/ascii.txt +++ /dev/null @@ -1,31 +0,0 @@ - π∞≠≠≈≠≠=≠≈∞∞∞∞∞ππ - π∞∞ π∞∞π √∞ √≠∞ - ≈≈ √≠ π∞π √πππ∞∞ - ∞≈ √∞ ∞π ∞π πππ= - ∞π π∞≈√√√√√√ √ π =π - π∞ π≠ππ√√ √π π≠ - ≈≠≠√√ √≈π ππ ∞√ - √=≈=≠==≠≠≠∞≈∞∞π π∞∞π √∞ - π≠∞π≈≠≈≈√∞≠=≠≠≠≠∞√ ∞√√∞ - ∞∞ ππ π π∞≈≈≠π π≠ - ≈≠∞√ =∞ π=≠√∞∞ ≠= - ∞√ππ π÷√ ∞≈∞≈∞∞≈≠≈= - √ π∞√ π≠π∞÷≠≠π - ∞π √π π≠÷≈π - π∞∞π √∞π - √≈√ πππ√∞∞∞π - π≠√ π∞ - ∞≠ π∞∞π∞ - π≠ ≠ ∞ ∞π - ≈×π∞×=π ∞≠≠√ - ∞∞≠≈ ∞÷≠∞∞∞√×∞ π∞ - ≠π ≈≈≈√∞∞≈π ≈√ ∞×≈ - π≠√ π≠π =√ √∞ π≠∞ππ∞ - ≈π≈≈π ≈π√≈ ∞≈π≠√ π≈≈√ - √≈∞π ∞√ ≠∞ ππ≈≈∞π π√ - ≠≠≠≠∞√√≈≈√π π≠≠=÷÷= - π=≠≠∞≈√ π≈ √≠ √≠π -√ππ∞≈√π√∞∞ ∞=≠≠=÷∞≠≈π - π ∞∞ππ - π∞π√∞ - ππππ diff --git a/cmd/webserver/public/reset.css b/cmd/webserver/public/reset.css deleted file mode 100644 index b1a0ecd..0000000 --- a/cmd/webserver/public/reset.css +++ /dev/null @@ -1,120 +0,0 @@ -/* Remove default margin */ -* { - margin: 0; - padding: 0; - border: 0; - font: inherit; - font-size: 100%; - vertical-align: baseline; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - text-size-adjust: none; -} - -/* Box sizing rules */ -*, -*::before, -*::after { - box-sizing: border-box; -} - -/* Remove default margin */ -body, -h1, -h2, -h3, -h4, -p, -figure, -blockquote, -dl, -dd { - margin-block-end: 0; -} - -/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */ -ul[role='list'], -ol[role='list'] { - list-style: none; -} - -/* Set core root defaults */ -html:focus-within { - scroll-behavior: smooth; -} - -html { - height: 100%; -} - -/* Set core body defaults */ -body { - min-height: 100%; - width: 100vw; - max-width: 100%; - box-sizing: border-box; - position: relative; - text-rendering: optimizeSpeed; - line-height: 1.5; -} - -/* A elements that don't have a class get default styles */ -a:not([class]) { - text-decoration-skip-ink: auto; -} - -/* Make images easier to work with */ -img, -picture, -svg { - max-width: 100%; - display: block; -} - -footer, -header, -nav, -section, -main { - display: block; -} - -blockquote, -q { - quotes: none; -} - -blockquote:before, -blockquote:after, -q:before, -q:after { - content: ''; - content: none; -} - -table { - border-collapse: collapse; - border-spacing: 0; -} - -input { - -webkit-appearance: none; - appearance: none; - border-radius: 0; -} - -/* Remove all animations, transitions and smooth scroll for people that prefer not to see them */ -@media (prefers-reduced-motion: reduce) { - html:focus-within { - scroll-behavior: auto; - } - - *, - *::before, - *::after { - animation-duration: 0.01ms !important; - animation-iteration-count: 1 !important; - transition-duration: 0.01ms !important; - scroll-behavior: auto !important; - } -} diff --git a/cmd/webserver/public/variables.css b/cmd/webserver/public/variables.css deleted file mode 100644 index a4bcfe1..0000000 --- a/cmd/webserver/public/variables.css +++ /dev/null @@ -1,49 +0,0 @@ -:root { - /* font size */ - --fs-100: clamp(0.7813rem, 0.7748rem + 0.0325vw, 0.8rem); - --fs-200: clamp(0.9375rem, 0.9159rem + 0.1085vw, 1rem); - --fs-300: clamp(1.125rem, 1.0819rem + 0.2169vw, 1.25rem); - --fs-400: clamp(1.35rem, 1.2767rem + 0.3688vw, 1.5625rem); - --fs-500: clamp(1.62rem, 1.5051rem + 0.5781vw, 1.9531rem); - --fs-600: clamp(1.9438rem, 1.7722rem + 0.8633vw, 2.4413rem); - --fs-700: clamp(2.3325rem, 2.0844rem + 1.2484vw, 3.0519rem); - --fs-800: clamp(2.7994rem, 2.4491rem + 1.7625vw, 3.815rem); - - /* @link https://utopia.fyi/space/calculator?c=320,18,1.2,1280,20,1.25,5,2,&s=0.75|0.5|0.25,1.5|2|3|4|6,s-l&g=s,l,xl,12 */ - - /* space */ - --space-3xs: clamp(0.3125rem, 0.3125rem + 0vi, 0.3125rem); - --space-2xs: clamp(0.5625rem, 0.5417rem + 0.1042vi, 0.625rem); - --space-xs: clamp(0.875rem, 0.8542rem + 0.1042vi, 0.9375rem); - --space-s: clamp(1.125rem, 1.0833rem + 0.2083vi, 1.25rem); - --space-m: clamp(1.6875rem, 1.625rem + 0.3125vi, 1.875rem); - --space-l: clamp(2.25rem, 2.1667rem + 0.4167vi, 2.5rem); - --space-xl: clamp(3.375rem, 3.25rem + 0.625vi, 3.75rem); - --space-2xl: clamp(4.5rem, 4.3333rem + 0.8333vi, 5rem); - --space-3xl: clamp(6.75rem, 6.5rem + 1.25vi, 7.5rem); - - /* One-up pairs */ - --space-3xs-2xs: clamp(0.3125rem, 0.2083rem + 0.5208vi, 0.625rem); - --space-2xs-xs: clamp(0.5625rem, 0.4375rem + 0.625vi, 0.9375rem); - --space-xs-s: clamp(0.875rem, 0.75rem + 0.625vi, 1.25rem); - --space-s-m: clamp(1.125rem, 0.875rem + 1.25vi, 1.875rem); - --space-m-l: clamp(1.6875rem, 1.4167rem + 1.3542vi, 2.5rem); - --space-l-xl: clamp(2.25rem, 1.75rem + 2.5vi, 3.75rem); - --space-xl-2xl: clamp(3.375rem, 2.8333rem + 2.7083vi, 5rem); - --space-2xl-3xl: clamp(4.5rem, 3.5rem + 5vi, 7.5rem); - - /* Two-up pairs */ - --space-3xs-xs: clamp(0.3125rem, 0.1042rem + 1.0417vi, 0.9375rem); - --space-2xs-s: clamp(0.5625rem, 0.3333rem + 1.1458vi, 1.25rem); - --space-xs-m: clamp(0.875rem, 0.5417rem + 1.6667vi, 1.875rem); - --space-s-l: clamp(1.125rem, 0.6667rem + 2.2917vi, 2.5rem); - --space-m-xl: clamp(1.6875rem, 1rem + 3.4375vi, 3.75rem); - --space-l-2xl: clamp(2.25rem, 1.3333rem + 4.5833vi, 5rem); - --space-xl-3xl: clamp(3.375rem, 2rem + 6.875vi, 7.5rem); - - /* Colors */ - --clr-base: #faf4ed; - --clr-text: #575279; - --clr-link: #907aa9; - --clr-high: #cecacd; -} diff --git a/flake.lock b/flake.lock index 718eee5..e3014f4 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1726560853, + "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", "type": "github" }, "original": { @@ -18,6 +18,60 @@ "type": "github" } }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_3": { + "locked": { + "lastModified": 1667395993, + "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "templ", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1709087332, + "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, "gomod2nix": { "inputs": { "flake-utils": [ @@ -28,11 +82,33 @@ ] }, "locked": { - "lastModified": 1725515722, - "narHash": "sha256-+gljgHaflZhQXtr3WjJrGn8NXv7MruVPAORSufuCFnw=", + "lastModified": 1728509152, + "narHash": "sha256-tQo1rg3TlwgyI8eHnLvZSlQx9d/o2Rb4oF16TfaTOw0=", + "owner": "nix-community", + "repo": "gomod2nix", + "rev": "d5547e530464c562324f171006fc8f639aa01c9f", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "gomod2nix", + "type": "github" + } + }, + "gomod2nix_2": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": [ + "templ", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1722589758, + "narHash": "sha256-sbbA8b6Q2vB/t/r1znHawoXLysCyD4L/6n6/RykiSnA=", "owner": "nix-community", "repo": "gomod2nix", - "rev": "1c6fd4e862bf2f249c9114ad625c64c6c29a8a08", + "rev": "4e08ca09253ef996bd4c03afa383b23e35fe28a1", "type": "github" }, "original": { @@ -43,11 +119,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1725432240, - "narHash": "sha256-+yj+xgsfZaErbfYM3T+QvEE2hU7UuE+Jf0fJCJ8uPS0=", + "lastModified": 1728888510, + "narHash": "sha256-nsNdSldaAyu6PE3YUA+YQLqUDJh+gRbBooMMekZJwvI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "ad416d066ca1222956472ab7d0555a6946746a80", + "rev": "a3c0b3b21515f74fd2665903d4ce6bc4dc81c77c", "type": "github" }, "original": { @@ -57,11 +133,28 @@ "type": "github" } }, + "nixpkgs_2": { + "locked": { + "lastModified": 1724322575, + "narHash": "sha256-kRYwAdYsaICNb2WYcWtBFG6caSuT0v/vTAyR8ap0IR0=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "2a02822b466ffb9f1c02d07c5dd6b96d08b56c6b", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "release-24.05", + "repo": "nixpkgs", + "type": "github" + } + }, "root": { "inputs": { "flake-utils": "flake-utils", "gomod2nix": "gomod2nix", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "templ": "templ" } }, "systems": { @@ -78,6 +171,64 @@ "repo": "default", "type": "github" } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "templ": { + "inputs": { + "gitignore": "gitignore", + "gomod2nix": "gomod2nix_2", + "nixpkgs": "nixpkgs_2", + "xc": "xc" + }, + "locked": { + "lastModified": 1728899295, + "narHash": "sha256-hV+vZEZW/EgfkrVke4+rBiaisgIApTKpYFM0MbmfM7Q=", + "owner": "a-h", + "repo": "templ", + "rev": "bb5e41b1cc6d4edb5cb2e173650320bb28fd74e0", + "type": "github" + }, + "original": { + "owner": "a-h", + "repo": "templ", + "type": "github" + } + }, + "xc": { + "inputs": { + "flake-utils": "flake-utils_3", + "nixpkgs": [ + "templ", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1724404748, + "narHash": "sha256-p6rXzNiDm2uBvO1MLzC5pJp/0zRNzj/snBzZI0ce62s=", + "owner": "joerdav", + "repo": "xc", + "rev": "960ff9f109d47a19122cfb015721a76e3a0f23a2", + "type": "github" + }, + "original": { + "owner": "joerdav", + "repo": "xc", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index ea6bf0c..d513c41 100644 --- a/flake.nix +++ b/flake.nix @@ -11,6 +11,7 @@ flake-utils.follows = "flake-utils"; }; }; + templ.url = "github:a-h/templ"; }; outputs = @@ -19,45 +20,66 @@ nixpkgs, flake-utils, gomod2nix, + templ, }: (flake-utils.lib.eachDefaultSystem ( system: let pkgs = nixpkgs.legacyPackages.${system}; + gopkgs = gomod2nix.legacyPackages.${system}; + templpkgs = templ.packages.${system}.templ; callPackage = pkgs.darwin.apple_sdk_11_0.callPackage or pkgs.callPackage; - - odin = callPackage ./nix/odin.nix { - MacOSX-SDK = pkgs.darwin.apple_sdk; - inherit (pkgs.darwin) Security; - }; in rec { - packages.default = callPackage ./nix/webserver.nix { - pname = "webserver"; - version = "0.1"; - inherit pkgs odin; - inherit (gomod2nix.legacyPackages.${system}) buildGoApplication; + packages.default = callPackage ./services/webserver { + inherit (pkgs) makeWrapper tailwindcss; + inherit (gopkgs) buildGoApplication; + inherit templpkgs; }; - packages.uploader = callPackage ./nix/uploader.nix { - pname = "uploader"; - version = "0.1"; - inherit (gomod2nix.legacyPackages.${system}) buildGoApplication; - }; + packages.uploader = callPackage ./cmd/uploader { inherit (gopkgs) buildGoApplication; }; - packages.container = callPackage ./nix/container.nix { - derivation = packages.default; + packages.blob = callPackage ./services/blob { inherit (gopkgs) buildGoApplication; }; - inherit pkgs; + packages.container = pkgs.dockerTools.buildImage { + name = packages.default.pname; + tag = packages.default.version; + created = "now"; + copyToRoot = pkgs.buildEnv { + name = "image-root"; + paths = [ packages.default ]; + pathsToLink = [ + "/bin" + "/public" + ]; + }; + config = { + Cmd = [ "${packages.default}/bin/${packages.default.pname}" ]; + Env = [ "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" ]; + ExposedPorts = { + "8080/tcp" = { }; + }; + }; }; - devShells.default = callPackage ./nix/shell.nix { - uploader = packages.uploader; + devShells.default = + let + goEnv = gopkgs.mkGoEnv { pwd = ./.; }; + in + pkgs.mkShell { + packages = [ + goEnv + gopkgs.gomod2nix + pkgs.air + pkgs.turso-cli + pkgs.gopls + pkgs.tailwindcss + # uploader + templpkgs + ]; + }; - inherit odin; - inherit (gomod2nix.legacyPackages.${system}) mkGoEnv gomod2nix; - }; } )); } diff --git a/go.mod b/go.mod index 9e01443..14a0533 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( ) require ( + github.com/a-h/templ v0.2.778 // indirect github.com/alecthomas/chroma/v2 v2.9.1 // indirect github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/coder/websocket v1.8.12 // indirect diff --git a/go.sum b/go.sum index bf8e132..d896ca6 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/a-h/templ v0.2.778 h1:VzhOuvWECrwOec4790lcLlZpP4Iptt5Q4K9aFxQmtaM= +github.com/a-h/templ v0.2.778/go.mod h1:lq48JXoUvuQrU0VThrK31yFwdRjTCnIE5bcPCM9IP1w= github.com/alecthomas/assert/v2 v2.2.1 h1:XivOgYcduV98QCahG8T5XTezV5bylXe+lBxLG2K2ink= github.com/alecthomas/assert/v2 v2.2.1/go.mod h1:pXcQ2Asjp247dahGEmsZ6ru0UVwnkhktn7S0bBDLxvQ= github.com/alecthomas/chroma/v2 v2.2.0/go.mod h1:vf4zrexSH54oEjJ7EdB65tGNHmH3pGZmVkgTP5RHvAs= diff --git a/gomod2nix.toml b/gomod2nix.toml index f25b857..35642de 100644 --- a/gomod2nix.toml +++ b/gomod2nix.toml @@ -1,6 +1,9 @@ schema = 3 [mod] + [mod."github.com/a-h/templ"] + version = "v0.2.778" + hash = "sha256-54iSeNJU4dMMkxHEh0KyRu//vADMnt93fjmXzqc2xPE=" [mod."github.com/alecthomas/chroma/v2"] version = "v2.9.1" hash = "sha256-rqRjAGKEBtCzoNe3zS9x+J3CM2EwzQAQli5/DWEGtJo=" diff --git a/nix/client.nix b/nix/client.nix deleted file mode 100644 index 8e44252..0000000 --- a/nix/client.nix +++ /dev/null @@ -1,25 +0,0 @@ -{ pkgs, odin }: -pkgs.stdenv.mkDerivation rec { - pname = "client"; - version = "0.1"; - src = ../client; - - nativeBuildInputs = [ odin ]; - - buildPhase = '' - mkdir -p $out/wasm - odin build $src -show-timings -out:$out/wasm/${pname}.wasm -no-bounds-check -o:size -target:js_wasm32 - - mkdir -p $out/js - cp ${odin.src}/vendor/wgpu/wgpu.js $out/js - cp ${odin.src}/vendor/wasm/js/runtime.js $out/js - - touch $out/bin - ''; - - doCheck = true; - - checkPhase = '' - odin test $src - ''; -} diff --git a/nix/container.nix b/nix/container.nix deleted file mode 100644 index 461d8f1..0000000 --- a/nix/container.nix +++ /dev/null @@ -1,21 +0,0 @@ -{ pkgs, derivation }: -pkgs.dockerTools.buildImage { - name = derivation.pname; - tag = derivation.version; - created = "now"; - copyToRoot = pkgs.buildEnv { - name = "image-root"; - paths = [ derivation ]; - pathsToLink = [ - "/bin" - "/cmd/${derivation.pname}" - ]; - }; - config = { - Cmd = [ "${derivation}/bin/${derivation.pname}" ]; - Env = [ "SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt" ]; - ExposedPorts = { - "8080/tcp" = { }; - }; - }; -} diff --git a/nix/odin.nix b/nix/odin.nix deleted file mode 100644 index 9b6dc19..0000000 --- a/nix/odin.nix +++ /dev/null @@ -1,94 +0,0 @@ -{ - lib, - fetchFromGitHub, - llvmPackages_17, - makeBinaryWrapper, - libiconv, - MacOSX-SDK, - Security, - which, -}: - -let - llvmPackages = llvmPackages_17; - inherit (llvmPackages) stdenv; -in -stdenv.mkDerivation rec { - pname = "odin"; - version = "dev-2024-09"; - - src = fetchFromGitHub { - owner = "odin-lang"; - repo = "Odin"; - rev = version; - hash = "sha256-rbKaGj4jwR+SySt+XJ7K9rtpQsL60IKJ55/1uNkVE1U="; - }; - - nativeBuildInputs = [ - makeBinaryWrapper - which - ]; - - buildInputs = lib.optionals stdenv.isDarwin [ - libiconv - Security - ]; - - LLVM_CONFIG = "${llvmPackages.llvm.dev}/bin/llvm-config"; - - postPatch = - lib.optionalString stdenv.isDarwin '' - substituteInPlace src/linker.cpp \ - --replace-fail '/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk' ${MacOSX-SDK} - '' - + '' - substituteInPlace build_odin.sh \ - --replace-fail '-framework System' '-lSystem' - patchShebangs build_odin.sh - ''; - - dontConfigure = true; - - buildFlags = [ "release" ]; - - installPhase = '' - runHook preInstall - - mkdir -p $out/bin - cp odin $out/bin/odin - - mkdir -p $out/share - cp -r base $out/share/base - cp -r core $out/share/core - cp -r vendor $out/share/vendor - - wrapProgram $out/bin/odin \ - --prefix PATH : ${ - lib.makeBinPath ( - with llvmPackages; - [ - bintools - llvm - clang - lld - ] - ) - } \ - --set-default ODIN_ROOT $out/share - - runHook postInstall - ''; - - meta = with lib; { - description = "A fast, concise, readable, pragmatic and open sourced programming language"; - mainProgram = "odin"; - homepage = "https://odin-lang.org/"; - license = licenses.bsd3; - maintainers = with maintainers; [ - luc65r - astavie - znaniye - ]; - platforms = platforms.x86_64 ++ [ "aarch64-darwin" ]; - }; -} diff --git a/nix/ols.nix b/nix/ols.nix deleted file mode 100644 index db7ff0e..0000000 --- a/nix/ols.nix +++ /dev/null @@ -1,18 +0,0 @@ -{ pkgs, odin }: - -let - ols = pkgs.ols.overrideAttrs ( - finalAttrs: previousAttrs: { - buildInputs = [ odin ]; - env.ODIN_ROOT = "${odin}/share"; - } - ); -in -pkgs.mkShell { - packages = [ - odin - ols - ]; - - env.ODIN_ROOT = "${odin}/share"; -} diff --git a/nix/shell.nix b/nix/shell.nix deleted file mode 100644 index dfcd250..0000000 --- a/nix/shell.nix +++ /dev/null @@ -1,32 +0,0 @@ -{ - pkgs, - mkGoEnv, - gomod2nix, - uploader, - odin, -}: - -let - ols = pkgs.ols.overrideAttrs ( - finalAttrs: previousAttrs: { - buildInputs = [ odin ]; - env.ODIN_ROOT = "${odin}/share"; - } - ); - - goEnv = mkGoEnv { pwd = ../.; }; -in -pkgs.mkShell { - packages = [ - goEnv - gomod2nix - pkgs.air - pkgs.turso-cli - uploader - - odin - ols - ]; - - env.ODIN_ROOT = "${odin}/share"; -} diff --git a/nix/uploader.nix b/nix/uploader.nix deleted file mode 100644 index f8767f6..0000000 --- a/nix/uploader.nix +++ /dev/null @@ -1,13 +0,0 @@ -{ - pname, - version, - buildGoApplication, -}: - -buildGoApplication { - inherit pname version; - src = ../.; - pwd = ../.; - modules = ../gomod2nix.toml; - subPackages = [ "cmd/${pname}" ]; -} diff --git a/nix/webserver.nix b/nix/webserver.nix deleted file mode 100644 index e0ce6f0..0000000 --- a/nix/webserver.nix +++ /dev/null @@ -1,50 +0,0 @@ -{ - pname, - version, - pkgs, - fetchurl, - buildGoApplication, - odin, -}: -let - htmxVersion = "2.0.2"; - htmx = fetchurl { - url = "https://github.com/bigskysoftware/htmx/releases/download/v${htmxVersion}/htmx.min.js"; - hash = "sha256-4XRtl1nsDUPFwoRFIzOjELtf1yheusSy3Jv0TXK1qIc="; - }; - - client = pkgs.callPackage ./client.nix { inherit odin; }; -in -buildGoApplication { - inherit pname version; - src = ../.; - pwd = ../.; - modules = ../gomod2nix.toml; - subPackages = [ "cmd/${pname}" ]; - nativeBuildInputs = [ pkgs.lightningcss ]; - postInstall = '' - static=$out/cmd/webserver/public - - mkdir -p $static - - rsync -a ./cmd/webserver/public $out/cmd/webserver --exclude styles --exclude js --exclude wasm --exclude='*.css' - - mkdir -p $static/js - cp ${htmx} $static/js/htmx.min.js - cp -r ${client.out}/js/* $static/js - - mkdir -p $static/wasm - cp -r ${client.out}/wasm/* $static/wasm - - lightningcss --minify --bundle ./cmd/webserver/main.css -t "> .5% or last 2 versions" -o $out/cmd/webserver/main.css - - find ./cmd/${pname} -name "*.tmpl" -exec sh -c ' - for file do - dest="$out/cmd/${pname}/''${file#./cmd/${pname}/}" - echo $(dirname "$dest") - mkdir -p "$(dirname "$dest")" - cp "$file" "$dest" - done - ' sh {} + - ''; -} diff --git a/services/blob/auth.go b/services/blob/auth.go new file mode 100644 index 0000000..63b6868 --- /dev/null +++ b/services/blob/auth.go @@ -0,0 +1,32 @@ +package main + +import ( + "log" + "net/http" + "strings" +) + +func BearerAuth(handler http.HandlerFunc, validToken string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + auth := r.Header.Get("Authorization") + if auth == "" { + log.Printf("auth is %s", auth) + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + token := strings.TrimPrefix(auth, "Bearer ") + if token == auth { + http.Error(w, "invalid authorization format", http.StatusUnauthorized) + return + } + + if token != validToken { + log.Printf("auth is %s, not %s", auth, validToken) + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + handler.ServeHTTP(w, r) + } +} diff --git a/services/blob/default.nix b/services/blob/default.nix new file mode 100644 index 0000000..b9ac9da --- /dev/null +++ b/services/blob/default.nix @@ -0,0 +1,9 @@ +{ buildGoApplication }: +buildGoApplication rec { + pname = "blob"; + version = "0.1.0"; + src = ../../.; + pwd = ./.; + modules = ../../gomod2nix.toml; + subPackages = [ "services/${pname}" ]; +} diff --git a/services/blob/handlers.go b/services/blob/handlers.go new file mode 100644 index 0000000..f191b9c --- /dev/null +++ b/services/blob/handlers.go @@ -0,0 +1,114 @@ +package main + +import ( + "encoding/json" + "io" + "log" + "mime" + "net/http" + "os" + "path/filepath" + "strings" +) + +const ( + uploadLimit = 10 << 20 +) + +func handlerDelete(w http.ResponseWriter, r *http.Request) { + filename := strings.TrimPrefix(r.URL.Path, "/files/") + if filename == "" { + http.Error(w, "filename not specified", http.StatusBadRequest) + return + } + + if err := FileDelete(filename); err != nil { + if os.IsNotExist(err) { + http.Error(w, "file not found", http.StatusNotFound) + } else { + http.Error(w, "failed to delete file", http.StatusInternalServerError) + log.Printf("blob-service: error deleting file (%v)", err) + } + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("file deleted successfully")) +} + +func handlerDownload(w http.ResponseWriter, r *http.Request) { + filename := strings.TrimPrefix(r.URL.Path, "/files/") + if filename == "" { + http.Error(w, "filename not specified", http.StatusBadRequest) + return + } + + file, err := FileGet(filename) + if err != nil { + if os.IsNotExist(err) { + http.Error(w, "file not found", http.StatusNotFound) + } else { + http.Error(w, "failed to retrieve file", http.StatusInternalServerError) + log.Printf("blob-service: error retrieving file (%v)", err) + } + return + } + defer file.Close() + + w.Header().Set("Content-Disposition", "attachment; filename="+filename) + + contentType := mime.TypeByExtension(filepath.Ext(filename)) + if contentType == "" { + w.Header().Set("Content-Type", "application/octet-stream") + } + w.Header().Set("Content-Type", contentType) + + if _, err = io.Copy(w, file); err != nil { + http.Error(w, "failed to send file", http.StatusInternalServerError) + log.Printf("blob-service: error sending file (%v)", err) + } +} + +func handlerUpload(w http.ResponseWriter, r *http.Request) { + r.Body = http.MaxBytesReader(w, r.Body, uploadLimit) + + filename := strings.TrimPrefix(r.URL.Path, "/files/") + if filename == "" { + http.Error(w, "filename not specified", http.StatusBadRequest) + return + } + + err := r.ParseMultipartForm(uploadLimit) + if err != nil { + http.Error(w, "failed to parse form data", http.StatusBadRequest) + return + } + + file, _, err := r.FormFile("file") + if err != nil { + http.Error(w, "failed to get file from request", http.StatusBadRequest) + return + } + defer file.Close() + + if err = FileSave(filename, file); err != nil { + http.Error(w, "failed to save file", http.StatusInternalServerError) + log.Printf("blob-service: error saving file (%v)", err) + return + } + + w.WriteHeader(http.StatusOK) + w.Write([]byte("file uploaded successfully")) +} + +func handlerList(w http.ResponseWriter, r *http.Request) { + files, err := FilesList() + if err != nil { + http.Error(w, "failed to list files", http.StatusInternalServerError) + log.Printf("blob-service: error listing files (%v)", err) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(files) +} diff --git a/services/blob/main.go b/services/blob/main.go new file mode 100644 index 0000000..be37e53 --- /dev/null +++ b/services/blob/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "log" + "net/http" + "os" +) + +const ( + Port = ":8080" +) + +var ( + Token = os.Getenv("BLOB_TOKEN") +) + +func main() { + if Token == "" { + log.Fatalf("blob-service: no token set, set BLOB_TOKEN environment var") + } + + http.HandleFunc("DELETE /files/", BearerAuth(handlerDelete, Token)) + http.HandleFunc("GET /files/", rateLimit(handlerDownload, 100)) + http.HandleFunc("PUT /files/", BearerAuth(handlerUpload, Token)) + + http.HandleFunc("GET /files", rateLimit(handlerList, 10)) + + log.Printf("blob-service: server started on port %s", Port) + err := http.ListenAndServe(Port, nil) + if err != nil { + log.Fatalf("blob-service: server failed (%v)", err) + } +} diff --git a/services/blob/ratelimiter.go b/services/blob/ratelimiter.go new file mode 100644 index 0000000..626c0bf --- /dev/null +++ b/services/blob/ratelimiter.go @@ -0,0 +1,43 @@ +package main + +import ( + "net" + "net/http" + "sync" + "time" +) + +const duration = time.Minute + +var ( + clients = make(map[string]int) + clientsMu = sync.Mutex{} +) + +func rateLimit(next http.HandlerFunc, maxCount int) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ip, _, _ := net.SplitHostPort(r.RemoteAddr) + count := clients[ip] + + if count > maxCount { + http.Error(w, "too many requests...", http.StatusTooManyRequests) + return + } + + clientsMu.Lock() + clients[ip]++ + clientsMu.Unlock() + + next.ServeHTTP(w, r) + + go func() { + time.Sleep(duration) + clientsMu.Lock() + clients[ip]-- + if clients[ip] <= 0 { + delete(clients, ip) + } + clientsMu.Unlock() + }() + } +} diff --git a/services/blob/storage.go b/services/blob/storage.go new file mode 100644 index 0000000..016dd35 --- /dev/null +++ b/services/blob/storage.go @@ -0,0 +1,59 @@ +package main + +import ( + "io" + "os" + "path/filepath" + "strings" +) + +const storageDir = "data" + +func init() { + if _, err := os.Stat(storageDir); os.IsNotExist(err) { + os.Mkdir(storageDir, 0755) + } +} + +func FileDelete(filename string) error { + filePath := filepath.Join(storageDir, filename) + return os.Remove(filePath) +} + +func FileGet(filename string) (*os.File, error) { + filePath := filepath.Join(storageDir, filename) + return os.Open(filePath) +} + +func FileSave(filename string, data io.Reader) error { + filePath := filepath.Join(storageDir, filename) + file, err := os.Create(filePath) + if err != nil { + return err + } + defer file.Close() + + _, err = io.Copy(file, data) + return err +} + +func FilesList() ([]string, error) { + filenames := make([]string, 0) + if err := filepath.Walk(storageDir, + func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if !info.IsDir() { + filenames = append(filenames, strings.TrimPrefix(path, storageDir+"/")) + } + + return nil + }, + ); err != nil { + return nil, err + } + + return filenames, nil +} diff --git a/cmd/webserver/cache/cache.go b/services/webserver/cache/cache.go similarity index 100% rename from cmd/webserver/cache/cache.go rename to services/webserver/cache/cache.go diff --git a/services/webserver/components/footer.templ b/services/webserver/components/footer.templ new file mode 100644 index 0000000..1cdc218 --- /dev/null +++ b/services/webserver/components/footer.templ @@ -0,0 +1,9 @@ +package components + +type Footer struct{} + +templ (p Footer) View() { + +} diff --git a/services/webserver/components/header.templ b/services/webserver/components/header.templ new file mode 100644 index 0000000..61c5004 --- /dev/null +++ b/services/webserver/components/header.templ @@ -0,0 +1,13 @@ +package components + +type Header struct { + PageCurrent string + Pages []string +} + +templ (p Header) View() { + +} diff --git a/services/webserver/components/nav.templ b/services/webserver/components/nav.templ new file mode 100755 index 0000000..96c95c8 --- /dev/null +++ b/services/webserver/components/nav.templ @@ -0,0 +1,27 @@ +package components + +type Nav struct { + PageCurrent string + Pages []string +} + +templ (p Nav) View() { + +} diff --git a/services/webserver/default.nix b/services/webserver/default.nix new file mode 100644 index 0000000..6001816 --- /dev/null +++ b/services/webserver/default.nix @@ -0,0 +1,46 @@ +{ + fetchurl, + makeWrapper, + buildGoApplication, + tailwindcss, + templpkgs, +}: +let + htmxVersion = "2.0.2"; + htmx = fetchurl { + url = "https://github.com/bigskysoftware/htmx/releases/download/v${htmxVersion}/htmx.min.js"; + hash = "sha256-4XRtl1nsDUPFwoRFIzOjELtf1yheusSy3Jv0TXK1qIc="; + }; +in +buildGoApplication rec { + pname = "webserver"; + version = "0.1"; + src = ../../.; + pwd = ./.; + modules = ../../gomod2nix.toml; + subPackages = [ "services/${pname}" ]; + nativeBuildInputs = [ + makeWrapper + tailwindcss + ]; + preBuild = '' + ${templpkgs}/bin/templ generate . + ''; + postInstall = '' + public=./services/${pname}/public + static=$out/public + + mkdir -p $static + + tailwindcss -c $public/tailwind.config.js -i $public/main.css -o $static/main.css --minify + + rsync -a $public $out --exclude js --exclude='*.css' + + mkdir -p $static/js + cp ${htmx} $static/js/htmx.min.js + + mv $out/bin/${pname} $out/bin/.${pname}-unwrapped + makeWrapper $out/bin/.${pname}-unwrapped $out/bin/${pname} \ + --chdir $out + ''; +} diff --git a/services/webserver/layouts/base.templ b/services/webserver/layouts/base.templ new file mode 100644 index 0000000..244f468 --- /dev/null +++ b/services/webserver/layouts/base.templ @@ -0,0 +1,37 @@ +package layouts + +import "personal-website/services/webserver/components" + +type Base struct { + Pages []string + PageCurrent string + Title string +} + +templ (p Base) View() { + + + + + + + { p.Title } + + + + + + + + + @components.Header{ + PageCurrent: p.PageCurrent, + Pages: p.Pages, + }.View() +
    + { children... } +
    + @components.Footer{}.View() + + +} diff --git a/services/webserver/main.go b/services/webserver/main.go new file mode 100644 index 0000000..d77f22e --- /dev/null +++ b/services/webserver/main.go @@ -0,0 +1,124 @@ +package main + +import ( + "bytes" + "html/template" + "log" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/a-h/templ" + "github.com/yuin/goldmark" + highlighting "github.com/yuin/goldmark-highlighting/v2" + + "personal-website/internal" + "personal-website/services/webserver/cache" + "personal-website/services/webserver/pages" +) + +var ( + port = os.Getenv("WEBSERVER_PORT") +) + +var logRequests = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + log.Printf("%s %s\n", req.Method, req.URL) + http.DefaultServeMux.ServeHTTP(w, req) +}) + +func main() { + log.Println("Starting server...") + + cache.InitCache() + + http.HandleFunc("GET /healthy", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + navList := []string{"home", "blog", "projects"} + + pageHome := pages.Home{Pages: navList} + http.Handle("GET /", templ.Handler(pageHome.View())) + http.Handle("GET /home", templ.Handler(pageHome.View())) + + pageBlog := pages.Blog{Pages: navList} + http.HandleFunc("GET /blog", func(w http.ResponseWriter, r *http.Request) { + posts, err := cache.Cache.GetPosts() + if err != nil { + log.Printf("failed to fetch posts from cache (%v)", err) + } + + pageBlog.View(posts).Render(r.Context(), w) + }) + + pagePost := pages.Post{Pages: navList} + http.HandleFunc("GET /post/{slug}", func(w http.ResponseWriter, r *http.Request) { + slug := r.PathValue("slug") + + post, err := slugToHTML(slug) + if err != nil { + log.Printf("failed to get post %s from cache (%v)", slug, err) + w.WriteHeader(http.StatusInternalServerError) + } + + pagePost.View(post).Render(r.Context(), w) + }) + + pageProjects := pages.Projects{Pages: navList} + http.Handle("GET /projects", templ.Handler(pageProjects.View())) + + // static + http.Handle("GET /public/", http.StripPrefix("/public/", staticHandler(http.Dir("public")))) + http.Handle("GET /robots.txt", staticHandler(http.Dir("static/seo"))) + + log.Fatal(http.ListenAndServe(port, logRequests)) +} + +func slugToHTML(slug string) (internal.Post, error) { + post, err := cache.Cache.GetPost(slug) + if err != nil { + log.Printf("Error getting post %s: %v", slug, err) + return post, err + } + + mdRenderer := goldmark.New( + goldmark.WithExtensions( + highlighting.NewHighlighting( + highlighting.WithStyle("rose-pine"), + ), + ), + ) + + var buf bytes.Buffer + err = mdRenderer.Convert([]byte(post.Content), &buf) + if err != nil { + log.Printf("Error parsing post %s to markdown: %v", slug, err) + return post, err + } + + post.HTML = template.HTML(buf.String()) + + return post, nil +} + +func staticHandler(root http.FileSystem) http.HandlerFunc { + fileServer := http.FileServer(root) + + return func(w http.ResponseWriter, r *http.Request) { + ext := strings.ToLower(filepath.Ext(r.URL.Path)) + + switch ext { + case ".css": + w.Header().Set("Cache-Control", "public, max-age=1800, immutable") + case ".js": + w.Header().Set("Cache-Control", "public, max-age=604800, immutable") + case ".jpg", ".jpeg", ".png", ".gif", ".ico": + w.Header().Set("Cache-Control", "public, max-age=2592000, immutable") + default: + w.Header().Set("Cache-Control", "max-age=0") + } + + fileServer.ServeHTTP(w, r) + } +} diff --git a/services/webserver/pages/blog.templ b/services/webserver/pages/blog.templ new file mode 100644 index 0000000..30ee89d --- /dev/null +++ b/services/webserver/pages/blog.templ @@ -0,0 +1,38 @@ +package pages + +import ( + "personal-website/internal" + "personal-website/services/webserver/layouts" +) + +type Blog struct { + Pages []string +} + +templ (p Blog) View(posts []internal.Post) { + @layouts.Base{ + Pages: p.Pages, + PageCurrent: "blog", + Title: "Ethan Thoma \\ Blog", + }.View() { +
    +

    Blog

    + +
    + } +} diff --git a/services/webserver/pages/home.templ b/services/webserver/pages/home.templ new file mode 100644 index 0000000..0b91fa1 --- /dev/null +++ b/services/webserver/pages/home.templ @@ -0,0 +1,30 @@ +package pages + +import "personal-website/services/webserver/layouts" + +type Home struct { + Pages []string +} + +templ (p Home) View() { + @layouts.Base{ + Pages: p.Pages, + PageCurrent: "home", + Title: "Ethan Thoma", + }.View() { +
    +

    Ethan Thoma

    +

    + ML graduate student @ STASER Lab UBC +
    + Focused on NLP and RL research +

    +

    Contact:

    + +
    + } +} diff --git a/services/webserver/pages/post.templ b/services/webserver/pages/post.templ new file mode 100644 index 0000000..4bb1e75 --- /dev/null +++ b/services/webserver/pages/post.templ @@ -0,0 +1,22 @@ +package pages + +import ( + "personal-website/internal" + "personal-website/services/webserver/layouts" +) + +type Post struct { + Pages []string +} + +templ (p Post) View(post internal.Post) { + @layouts.Base{ + Pages: p.Pages, + PageCurrent: "blog", + Title: "Ethan Thoma \\ " + post.Title, + }.View() { +
    + @templ.Raw(post.HTML) +
    + } +} diff --git a/services/webserver/pages/projects.templ b/services/webserver/pages/projects.templ new file mode 100644 index 0000000..64a50cc --- /dev/null +++ b/services/webserver/pages/projects.templ @@ -0,0 +1,45 @@ +package pages + +import "personal-website/services/webserver/layouts" + +type Projects struct { + Pages []string +} + +templ (p Projects) View() { + @layouts.Base{ + Pages: p.Pages, + PageCurrent: "projects", + Title: "Ethan Thoma \\ Projects", + }.View() { +
    +

    Projects

    + +
    + } +} diff --git a/cmd/webserver/public/favicon/android-chrome-192x192.png b/services/webserver/public/favicon/android-chrome-192x192.png similarity index 100% rename from cmd/webserver/public/favicon/android-chrome-192x192.png rename to services/webserver/public/favicon/android-chrome-192x192.png diff --git a/cmd/webserver/public/favicon/android-chrome-512x512.png b/services/webserver/public/favicon/android-chrome-512x512.png similarity index 100% rename from cmd/webserver/public/favicon/android-chrome-512x512.png rename to services/webserver/public/favicon/android-chrome-512x512.png diff --git a/cmd/webserver/public/favicon/apple-touch-icon.png b/services/webserver/public/favicon/apple-touch-icon.png similarity index 100% rename from cmd/webserver/public/favicon/apple-touch-icon.png rename to services/webserver/public/favicon/apple-touch-icon.png diff --git a/cmd/webserver/public/favicon/favicon-16x16.png b/services/webserver/public/favicon/favicon-16x16.png similarity index 100% rename from cmd/webserver/public/favicon/favicon-16x16.png rename to services/webserver/public/favicon/favicon-16x16.png diff --git a/cmd/webserver/public/favicon/favicon-32x32.png b/services/webserver/public/favicon/favicon-32x32.png similarity index 100% rename from cmd/webserver/public/favicon/favicon-32x32.png rename to services/webserver/public/favicon/favicon-32x32.png diff --git a/cmd/webserver/public/favicon/favicon.ico b/services/webserver/public/favicon/favicon.ico similarity index 100% rename from cmd/webserver/public/favicon/favicon.ico rename to services/webserver/public/favicon/favicon.ico diff --git a/cmd/webserver/public/favicon/site.webmanifest b/services/webserver/public/favicon/site.webmanifest similarity index 100% rename from cmd/webserver/public/favicon/site.webmanifest rename to services/webserver/public/favicon/site.webmanifest diff --git a/services/webserver/public/main.css b/services/webserver/public/main.css new file mode 100644 index 0000000..1f881dc --- /dev/null +++ b/services/webserver/public/main.css @@ -0,0 +1,69 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer components { + .content { + &>*+* { + @apply mt-2xs-xs; + } + + & h1 { + @apply text-2xl; + @apply mb-m; + @apply font-extrabold; + @apply leading-none; + @apply tracking-tight; + } + + & h2 { + @apply text-lg; + @apply mt-l-xl; + @apply font-extrabold; + @apply leading-none; + @apply tracking-tight; + } + + & h3 { + @apply text-m; + @apply mt-l-xl; + @apply font-extrabold; + @apply leading-none; + @apply tracking-tight; + } + + & pre { + @apply bg-high; + @apply text-sm; + @apply overflow-x-auto; + @apply p-xs; + + &>code { + @apply break-words; + } + } + + & a { + @apply underline; + } + + & ul, + & ol { + @apply list-inside; + + &>li { + @apply ml-m; + } + + &>*+li { + @apply mt-3xs; + } + } + + & blockquote { + @apply border-l-4; + @apply border-link; + @apply pl-3xs; + } + } +} diff --git a/cmd/webserver/public/seo/robots.txt b/services/webserver/public/seo/robots.txt similarity index 100% rename from cmd/webserver/public/seo/robots.txt rename to services/webserver/public/seo/robots.txt diff --git a/cmd/webserver/public/seo/sitemap.xml b/services/webserver/public/seo/sitemap.xml similarity index 100% rename from cmd/webserver/public/seo/sitemap.xml rename to services/webserver/public/seo/sitemap.xml diff --git a/services/webserver/public/tailwind.config.js b/services/webserver/public/tailwind.config.js new file mode 100644 index 0000000..0e4e6dd --- /dev/null +++ b/services/webserver/public/tailwind.config.js @@ -0,0 +1,83 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ['./**/*.{html,templ,go}'], + theme: { + colors: { + 'base': '#fafaf8', + 'content': '#141413', + 'link': '#907aa9', + 'high': '#cecacd', + }, + + fontSize: { + // @link https://utopia.fyi/type/calculator?c=320,16,1.2,1240,22,1.25,5,2,&s=0.75|0.5|0.25,1.5|2|3|4|6|8|11|15|19,3xs-xs|2xs-s|xs-m|s-l|m-xl|l-2xl|xl-3xl|2xl-4xl|3xl-5xl|4xl-6xl|5xl-6xl|6xl-7xl&g=s,l,xl,12 + + 'xs': 'clamp(0.6944rem, 0.6299rem + 0.3227vw, 0.88rem)', + 'sm': 'clamp(0.9375rem, 0.9159rem + 0.1085vw, 1rem)', + 'm': 'clamp(1rem, 0.8696rem + 0.6522vw, 1.375rem)', + 'lg': 'clamp(1.35rem, 1.2767rem + 0.3688vw, 1.5625rem)', + 'xl': 'clamp(1.62rem, 1.5051rem + 0.5781vw, 1.9531rem)', + '2xl': 'clamp(1.9438rem, 1.7722rem + 0.8633vw, 2.4413rem)', + '3xl': 'clamp(2.3325rem, 2.0844rem + 1.2484vw, 3.0519rem)', + '4xl': 'clamp(2.7994rem, 2.4491rem + 1.7625vw, 3.815rem)', + }, + + spacing: { + // @link https://utopia.fyi/space/calculator?c=320,16,1.2,1240,22,1.25,5,2,&s=0.75|0.5|0.25,1.5|2|3|4|6|8|11|15|19,3xs-xs|2xs-s|xs-m|s-l|m-xl|l-2xl|xl-3xl|2xl-4xl|3xl-5xl|4xl-6xl|5xl-6xl|6xl-7xl|s-2xl&g=s,l,xl,12 + + '3xs': 'clamp(0.25rem, 0.2065rem + 0.2174vw, 0.375rem)', + '2xs': 'clamp(0.5rem, 0.4348rem + 0.3261vw, 0.6875rem)', + 'xs': 'clamp(0.75rem, 0.6413rem + 0.5435vw, 1.0625rem)', + 's': 'clamp(1rem, 0.8696rem + 0.6522vw, 1.375rem)', + 'm': 'clamp(1.5rem, 1.3043rem + 0.9783vw, 2.0625rem)', + 'l': 'clamp(2rem, 1.7391rem + 1.3043vw, 2.75rem)', + 'xl': 'clamp(3rem, 2.6087rem + 1.9565vw, 4.125rem)', + '2xl': 'clamp(4rem, 3.4783rem + 2.6087vw, 5.5rem)', + '3xl': 'clamp(6rem, 5.2174rem + 3.913vw, 8.25rem)', + '4xl': 'clamp(8rem, 6.9565rem + 5.2174vw, 11rem)', + '5xl': 'clamp(11rem, 9.5652rem + 7.1739vw, 15.125rem)', + '6xl': 'clamp(15rem, 13.0435rem + 9.7826vw, 20.625rem)', + '7xl': 'clamp(19rem, 16.5217rem + 12.3913vw, 26.125rem)', + + // One-up pairs + '3xs-2xs': 'clamp(0.25rem, 0.0978rem + 0.7609vw, 0.6875rem)', + '2xs-xs': 'clamp(0.5rem, 0.3043rem + 0.9783vw, 1.0625rem)', + 'xs-s': 'clamp(0.75rem, 0.5326rem + 1.087vw, 1.375rem)', + 's-m': 'clamp(1rem, 0.6304rem + 1.8478vw, 2.0625rem)', + 'm-l': 'clamp(1.5rem, 1.0652rem + 2.1739vw, 2.75rem)', + 'l-xl': 'clamp(2rem, 1.2609rem + 3.6957vw, 4.125rem)', + 'xl-2xl': 'clamp(3rem, 2.1304rem + 4.3478vw, 5.5rem)', + '2xl-3xl': 'clamp(4rem, 2.5217rem + 7.3913vw, 8.25rem)', + '3xl-4xl': 'clamp(6rem, 4.2609rem + 8.6957vw, 11rem)', + '4xl-5xl': 'clamp(8rem, 5.5217rem + 12.3913vw, 15.125rem)', + '5xl-6xl': 'clamp(11rem, 7.6522rem + 16.7391vw, 20.625rem)', + '6xl-7xl': 'clamp(15rem, 11.1304rem + 19.3478vw, 26.125rem)', + + // Two-up pairs + '3xs-xs': 'clamp(0.25rem, -0.0326rem + 1.413vw, 1.0625rem)', + '2xs-s': 'clamp(0.5rem, 0.1957rem + 1.5217vw, 1.375rem)', + 'xs-m': 'clamp(0.75rem, 0.2935rem + 2.2826vw, 2.0625rem)', + 's-l': 'clamp(1rem, 0.3913rem + 3.0435vw, 2.75rem)', + 'm-xl': 'clamp(1.5rem, 0.587rem + 4.5652vw, 4.125rem)', + 'l-2xl': 'clamp(2rem, 0.7826rem + 6.087vw, 5.5rem)', + 'xl-3xl': 'clamp(3rem, 1.1739rem + 9.1304vw, 8.25rem)', + '2xl-4xl': 'clamp(4rem, 1.5652rem + 12.1739vw, 11rem)', + '3xl-5xl': 'clamp(6rem, 2.8261rem + 15.8696vw, 15.125rem)', + '4xl-6xl': 'clamp(8rem, 3.6087rem + 21.9565vw, 20.625rem)', + '5xl-6xl': 'clamp(11rem, 7.6522rem + 16.7391vw, 20.625rem)', + '6xl-7xl': 'clamp(15rem, 11.1304rem + 19.3478vw, 26.125rem)', + + // Custom + 's-2xl': 'clamp(1rem, -0.5652rem + 7.8261vw, 5.5rem)', + }, + + extend: { + maxWidth: { + 'content': 'min(clamp(320px, 75%, 640px), 100svw)', + }, + }, + }, + corePlugins: { + preflight: true, + }, +}