Initial commit, survey laid out.

master
Zed A. Shaw 2 days ago
commit 29d3775f8c
  1. 37
      .gitignore
  2. 25
      .ozai.json
  3. 13
      .ssgod.json
  4. 1
      LICENSE
  5. 68
      Makefile
  6. 627
      README.md
  7. 3
      TODO.txt
  8. 75
      common/api.go
  9. 39
      common/auth.go
  10. 167
      common/email/api.go
  11. 28
      common/email/data.go
  12. 29
      common/errors.go
  13. 108
      common/web.go
  14. 54
      config/settings.go
  15. 29
      config_example.json
  16. 59
      data/crud.go
  17. 56
      data/models.go
  18. 10
      emails/signup.md
  19. 155
      features/admin/api.go
  20. 11
      features/admin/init.go
  21. 51
      features/admin/views.go
  22. 80
      features/auth/api.go
  23. 9
      features/auth/init.go
  24. 22
      features/email/api.go
  25. 10
      features/email/init.go
  26. 11
      features/email/views.go
  27. 13
      features/fakepay/api.go
  28. 10
      features/fakepay/init.go
  29. 11
      features/fakepay/views.go
  30. 22
      features/init.go
  31. 78
      features/paypal/api.go
  32. 160
      features/paypal/api.js
  33. 10
      features/paypal/init.go
  34. 11
      features/paypal/views.go
  35. 31
      features/shopping/api.go
  36. 10
      features/shopping/init.go
  37. 11
      features/shopping/views.go
  38. 15
      features/survey/api.go
  39. 10
      features/survey/init.go
  40. 11
      features/survey/views.go
  41. 64
      go.mod
  42. 137
      go.sum
  43. 68
      main.go
  44. 9
      migrations/20250802154952_init.sql
  45. 13
      migrations/20260112190943_shopping_init.sql
  46. 13
      migrations/20260112195518_shopping_cart.sql
  47. 5
      pages/about.md
  48. 2
      pages/error/index.html
  49. 91
      pages/examples/gitea.html
  50. 24
      pages/examples/index.html
  51. 97
      pages/examples/linkedin.html
  52. 154
      pages/examples/sample.html
  53. 3
      pages/examples/starter.html
  54. 155
      pages/examples/stride.html
  55. 203
      pages/examples/twitch.html
  56. 39
      pages/index.html
  57. 15
      pages/layouts/examples.html
  58. 35
      pages/login/index.html
  59. 20
      pages/register/index.html
  60. 9
      pages/sitemap.html
  61. 462
      static/default_theme.css
  62. BIN
      static/favicon.ico
  63. 15
      static/icons/LICENSE
  64. 17
      static/icons/accessibility.svg
  65. 13
      static/icons/activity.svg
  66. 16
      static/icons/air-vent.svg
  67. 14
      static/icons/airplay.svg
  68. 18
      static/icons/alarm-check.svg
  69. 18
      static/icons/alarm-clock-off.svg
  70. 18
      static/icons/alarm-clock.svg
  71. 18
      static/icons/alarm-minus.svg
  72. 19
      static/icons/alarm-plus.svg
  73. 14
      static/icons/album.svg
  74. 15
      static/icons/alert-circle.svg
  75. 15
      static/icons/alert-octagon.svg
  76. 15
      static/icons/alert-triangle.svg
  77. 17
      static/icons/align-center-horizontal.svg
  78. 17
      static/icons/align-center-vertical.svg
  79. 15
      static/icons/align-center.svg
  80. 15
      static/icons/align-end-horizontal.svg
  81. 15
      static/icons/align-end-vertical.svg
  82. 18
      static/icons/align-horizontal-distribute-center.svg
  83. 16
      static/icons/align-horizontal-distribute-end.svg
  84. 16
      static/icons/align-horizontal-distribute-start.svg
  85. 15
      static/icons/align-horizontal-justify-center.svg
  86. 15
      static/icons/align-horizontal-justify-end.svg
  87. 15
      static/icons/align-horizontal-justify-start.svg
  88. 15
      static/icons/align-horizontal-space-around.svg
  89. 16
      static/icons/align-horizontal-space-between.svg
  90. 15
      static/icons/align-justify.svg
  91. 15
      static/icons/align-left.svg
  92. 15
      static/icons/align-right.svg
  93. 15
      static/icons/align-start-horizontal.svg
  94. 15
      static/icons/align-start-vertical.svg
  95. 18
      static/icons/align-vertical-distribute-center.svg
  96. 16
      static/icons/align-vertical-distribute-end.svg
  97. 16
      static/icons/align-vertical-distribute-start.svg
  98. 15
      static/icons/align-vertical-justify-center.svg
  99. 15
      static/icons/align-vertical-justify-end.svg
  100. 15
      static/icons/align-vertical-justify-start.svg
  101. Some files were not shown because too many files have changed in this diff Show More

37
.gitignore vendored

@ -0,0 +1,37 @@
# ---> Vim
# Swap
[._]*.s[a-v][a-z]
!*.svg # comment out if you don't need vector files
[._]*.sw[a-p]
[._]s[a-rt-v][a-z]
[._]ss[a-gi-z]
[._]sw[a-p]
# Session
Session.vim
Sessionx.vim
# Temporary
.netrwhist
*~
# Auto-generated tag files
tags
# Persistent undo
[._]*.un~
backup
*.exe
*.dll
*.world
coverage
coverage/*
.venv
*.gz
config.json
public
*.idx
*.sqlite3
webapp
bin
static/style.css
db.*

@ -0,0 +1,25 @@
{
"HostPort": ":9999",
"Processes": {
"webapp": {
"URL": "/webapp",
"Command": "./bin/webapp",
"Args": []
},
"ssgod": {
"URL": "/ssgod",
"Command": "go",
"Args": ["tool", "ssgod", "watch"]
},
"mailhog": {
"URL": "/mailhog",
"Command": "MailHog",
"Args": []
},
"tailwind": {
"URL": "/tailwind",
"Command": "tailwindcss",
"Args": ["--input", "./static/input_style.css", "--output", "./static/style.css","--watch=always"]
}
}
}

@ -0,0 +1,13 @@
{
"views": "pages",
"layout": "views/layouts/main.html",
"target": "public",
"watch_delay": "500ms",
"sync_dir": "static",
"exclude": [
"pages/layouts/.*"
],
"meta_files": [
"pages/sitemap.html"
]
}

@ -0,0 +1 @@
It's Copyright. All Rights Reserved. You have no license at all other than permission to view it.

@ -0,0 +1,68 @@
all: build
ifeq '$(OS)' 'Windows_NT'
powershell -ExecutionPolicy bypass "./tools/restart.ps1"
else
curl http://127.0.0.1:9999/webapp || true
endif
build: cmds
go build -o bin/ .
cmds:
go build -o bin/ ./tools/cmd/fgen
go build -o bin/ ./tools/cmd/qmgr
go build -o bin/ ./tools/cmd/mailer
site:
go tool ssgod
test: build
mkdir -p bin/tests
go test ./tests/... -c -o bin/tests
./bin/tests/admin.test
./bin/tests/auth.test
./bin/tests/fakepay.test
./bin/tests/paypal.test
./bin/tests/shopping.test
test_only: cmds site
./bin/tests.test -test.run $(TEST)
migrate_up:
goose sqlite3 db.sqlite3 -dir migrations up
migrate_down:
goose sqlite3 db.sqlite3 -dir migrations down
docs:
pkgsite --open
fix_ubuntu_ulimit:
@echo "Run this in your shell:"
@echo "ulimit -n 65536"
tailwind:
tailwindcss --input ./static/input_style.css --output ./static/style.css
tailwind_watch:
tailwindcss --input ./static/input_style.css --output ./static/style.css --watch=always
tailwind_install:
curl -LO https://github.com/tailwindlabs/tailwindcss/releases/download/v4.1.12/tailwindcss-linux-x64
chmod oug+x tailwindcss-linux-x64
sudo mv tailwindcss-linux-x64 /usr/local/bin/tailwindcss
dev:
go tool ozai
coverage:
go build -cover -o webapp
mkdir -p .coverage
echo "GOCOVERDIR=.coverage ./webapp"
cover_report:
go tool covdata textfmt -i=.coverage -o coverage.txt
go tool cover -func=coverage.txt
go tool cover -html=coverage.txt -o coverage.html
open coverage.html

@ -0,0 +1,627 @@
# Go Web Starter Kit
This is a fairly complete web development starter kit in Go. It tries to be as simple as possible
without leaving out modern features. The goal is to create a setup that a normal person can learn
and study core web technologies like the DOM and web servers. I would classify it as, "Old school
with modern things."
## The Stack
Currently I'm using the following components in a tastefully crafted stack similar to a Crepe Cake:
* [goose](https://github.com/pressly/goose) -- This manages the database migrations.
* [sqlx](https://github.com/jmoiron/sqlx) -- This is the database driver.
* [squirrel](https://github.com/Masterminds/squirrel) -- This is the SQL query generator.
* [Fiber](https://gofiber.io/) -- This is the main web API and does most everything you need.
* [tailwind](https://tailwindcss.com/) -- This makes your sites perty and is easy to use, but I
reject the `@apply` slander and use `@apply` to give you nice things.
* [ssgod](https://lcthw.dev/go/ssgod) -- A static site generator I wrote that _only_ does static site generation and nothing else...unlike Hugo.
* [ozai](https://lcthw.dev/go/ozai) -- A process manager that replaced Air because Air went crazy like Hugo.
* [chromedp](https://github.com/chromedp/chromedp) -- This does your automated testing for you.
I then add a few little "sweetener" APIs on top of this for simple things like, making the tests
nicer or making it easier to quickly return a page from a view.
## Getting Started
First, install a couple tools that'll be used everywhere, and that have huge dependencies you don't
actually need in your project:
```shell
go install golang.org/x/pkgsite/cmd/pkgsite@latest
go install github.com/pressly/goose/v3/cmd/goose@latest
```
To start your own project do this:
```shell
git clone https://lcthw.dev/go/go-web-starter-kit.git my-project
cd my-project
```
Then on Linux/OSX you want to delete the `.git` with this:
```shell
rm -rf .git
mv LICENSE LICENSE.old # it's MIT
```
And on Windows you use the ever more clear and totally easier:
```shell
rm -recurse -force .git
mv LICENSE LICENSE.old # it's MIT
```
Once you have that you can make it your own:
```shell
git init .
go mod tidy
cp config_example.json config.json
make migrate_up
sqlite3 db.sqlite3 ".read tools/pragmas.sql"
make
make dev
```
This gets the site running and ready for development. You only need _to do this once._. The next
day that you want to work, cd to your project and only run `make dev`:
```shell
make dev
```
## Linux Bug?
I found that on Ubuntu the `tailwindcss` command "wouldn't run." Turns out when `tailwindcss` forks
it does the right thing and closes all possible open file descriptors, _but_ Ubuntu has set the hard
limit to 1 billion:
```shell
$ ulimit -Hn
1073741816
```
This is a _lot_ of files to try to close, so `tailwindcss` is actually stuck doing that. You can
fix this with:
```shell
ulimit -n 65536
```
You can pick any reasonable number, and then `tailwindcss` works like expected.
## Configuration
There's 3 basic moving parts to the whole system: `webapp`, `ssgod`, `tailwind`, and `ozai`.
You can configure the `webapp` using the `config.json` which you copied over from `config_example.json`. The options are fairly self-explanatory.
You can configure [ssgod](https://lcthw.dev/go/ssgod) using the
`ssgod.json` file. You'll notice that there's _two_ layouts with this setup, one for `webapp`
and another for `ssgod`. You can just point `ssgod` at your `views/layouts/main.html` file if you
want, but it seems people want two different layouts for their static site vs. "the app."
You can configure `tailwind` using the `static/input_style.css` and editing the `Makefile` to have
it run how you want. I use the `@apply` heavily because I'm a programmer and no programmer would
ever tell someone they should sprinkle repetitive bullshit all over their HTML. Resist the
authority. Use `@apply`.
Finally, you can configure other processes to run with [Ozai](https://lcthw.dev/go/ozai) in the
`.ozai.json` file. Why so many different formats? I'm still learning which is best to use and while
I like TOML I think I may go with `.json` files to reduce dependencies. You tell me though.
## Ozai Autorestarts
Ozai is a simpler tool that doesn't do auto restart. Instead it will create a little webserver at
`http://127.0.0.1:9999` with endpoints you can hit with curl to force a restart. Look in the
.ozai.json file to see what's configured. This makes it easy to simple add a `curl` call in your
build process and kick everything over. Look in the `Makefile` for how I do this.
This works better than the way `Air` does it for a few reasons:
1. You can run your build like normal in your own IDE or build tool and see your errors directly.
When you use other autobuild tools they have the errors so you can't access them.
2. You get an immediate restart after your build succeeds, rather than waiting for an external tool
to detect you made changes, run the build, and then restart your browser.
3. You avoid the "auto reload deadlock" where you run a build, the server is shutdown, but you hit
refresh too soon, so now your browser waits for an error, then you have to refresh again. Way
easier to just not use any of that.
4. No more silent failures hiding inside your terminal that you can't see. Just manually restart
it.
For more, checkout the [Ozai Project](https://lcthw.dev/go/ozai").
## Tour of Directories
The directories are organized in a way that separates "App Stuff" from "Content Stuff".
* `admin` <b>App</b> The code for the built-in database admin tool.
* `bin` <b>App</b> Binaries that run your app like `webapp`.
* `features` <b>App</b> Where you put your features.
* `common`<b>App</b> Common helper functions.
* `config` <b>App</b> The configuration for your site, which is `config.json` loaded into Go structs.
* `data` <b>App</b> Your database stuff. You put your SQL models in here, and then add them to the bottom `Map` to
make them magically show up in the `/admin/table/` tool.
* `migrations` <b>App</b> When you use [goose](https://github.com/pressly/goose) it puts the migrations in here.
* `tests` <b>App</b> Your tests go in here. I use [chromedp](https://github.com/chromedp/chromedp) but the `chromedp`
API is too low level (and weird) to be useful so look in `tests/tools.go` for what I use.
* `tools` <b>App</b> Various admin automation tools go here.
* `views` <b>Content</b> This is where you put the _App_ contents, not the static content. Sadly, this
should be in `features/` but Fiber makes that too difficult.
* `emails` <b>Content</b> This where you put templates for emails you'll send.
* `pages` <b>Content</b> These are templates that are generated once and then served statically.
* `static` <b>Static</b> This is static content like `.js`, images, and `.css`.
The following directories are considered "build junk" and you _should not_ commit them to your git.
* `public` <b>Junk</b> This is generated by `ssgod` and then served by your `webapp`. Don't
edit anything in here, and instead edit in `pages` or `static`.
* `tmp` <b>Junk</b> This is made during the build/testing process.
## Dev Workflow
The first thing I do is run each of these commands in different terminals, either with tmux or just
using my fingers to run multiple tabs in PowerShell:
```shell
make dev
```
### 1. Create a Feature
The first step is to create a `feature`, which is nothing more than some files in a directory that
you edit to make your `pages/` actually work. You can create a feature like this:
```shell
./bin/fgen -name myfeatue
```
This will create the files and directories you need to make a new feature:
`features/myfeature/`
: This contains all of the go code.
`views/myfeature/`
: This is where your HTML goes.
`tests/myfeature/`
: This is where your tests go.
Next you add it to `features/init.go` to enable it:
```diff
@@ -8,2 +8,3 @@ import (
"MY/webapp/features/fakepay"
+ "MY/webapp/features/myfeature"
)
@@ -15,2 +16,3 @@ func Setup(app *fiber.App) {
features_fakepay.Setup(app)
+ features_myfeature.Setup(app)
}
```
> __NOTE__: I'm going to automate this in the future, so expect this file to be generated.
After that run `make` to build everything again and you should have a new feature at
`http://127.0.0.1:7001/myfeature/`
with your browser.
### 2. Create Initial Views
The design of this system is that you do _not_ have to edit 16 files just get an idea going. You
can have an idea and create a single `.html` or `.md` file in `views/myfeature` to see it happen. This lets
you work out all of the URLs, page flows, layouts, and basic design before writing any more code.
This is generally a better way to work than creating the backend models first as it constrains what
you create to only exactly what you need for the UI you'll implement.
What I do is make a new file in `myfeature/views/`, cook up a quick 2D layout that's mostly what I want, then
make it link to other `myfeature/views/` to figure out the feature. Once I have a rough UI for the feature
worked out I move on to the next step.
### Using the Tailwind Starter
I've included [tailwind](https://tailwindcss.com/) in this setup, since it's decent at getting
things working quickly right in the HTML. The only thing I've fully rejected is the idea that
"@apply is bad m'kay." No, `@apply` makes it so I don't rage at all the idiots who think 92
repeated CSS classes on every `div` is "good practice."
> __NOTE__ You can view a sample page with all of these in [/examples/sample.html](/examples/sample.html) and you can look at the other files in `examples` to get an idea of what you can do. Some of these just use tailwind, others use my starter kit.
In my setup there's a `static/input_style.css` that is used to generate a `static/style.css` and it
comes preconfigured with many of the things you'll need to get a page up and running. In theory
you can prototype a first page with just HTML and a bit of tailwind as needed. It also uses many of
the stock HTML tags like `aside` and `mark` so you can keep things nice.
I've added 4 additional layout tags for you to use:
* `grid` -- For stuff in a grid. Add `class="grid-cols-2"` from tailwind to change its layout.
* `block` -- A simple pre-configured vertical flex box. Put it around vertical stuff.
* `bar` -- A simple pre-configured horizontal flex box. Pit it around horizontal stuff.
* `stack` -- A simple way to stack stuff on top of other stuff. You know, like in every single 2d compositional system since the 1500s.
* `shape` -- A simple 2d box to use for placeholders where you'll put images. It's a lot quicker to drop a little `shape` than to go find an image. I think it also makes it easier to focus on the design.
### 3. Create Fake Data
Create a fake set of data that comes from your page prototype. For example, if I have a User
Profile page then I might add this:
```html
<script type="module">
const data = {
"Username": "Frank",
"Email": "zed@zed.com",
}
</script>
```
This will give you something to work with while you sort out how the interactive parts of your page
will work. If you don't have any then you can skip this part.
### 4. Refine the UI Interactions
Using the fake data determine the user interactions for the view. Remember that this system is
designed to minimize the amount of work you need on the front-end, so all you're doing here is
simple things like validating a form, getting data, disabling/enabling fields, and anything that
stays _on one page_. If you find yourself trying to cram multiple pages into one then you've gone
too far.
With my simple use-profile example I could create the function that renders the table on page load:
```javascript
$boot(async() => {
const tmpl = $id('table-row');
const target = $id('table-items');
for(let key in data) {
$append(target,
$render(tmpl, {key, value: data[key]}));
}
});
```
This comes from my `jzed.js` file that's a minimalist old-school DOM helper. I also include
[lit.js](https://lit.dev/) if you prefer to create little components for things.
### 5. Move the Fake Data to `api.go`
You should be spending a lot of time working out the data and interactions in the nice convenience
of the page you're working on, but eventually that data needs to come from the server. To continue
the _User Profile_ example we can create a handler in `api.go` that returns this same fake data:
```go
package features_myfeature
import (
"github.com/gofiber/fiber/v2"
)
type UserProfile struct {
UserName string
Email string
}
func GetApiUserProfile(c *fiber.Ctx) error {
profile := UserProfile{
UserName: "Zed",
Email: "zed@zed.com",
}
return c.JSON(profile)
}
func SetupApi(app *fiber.App) {
app.Get("/api/myfeature/user_profile", GetApiUserProfile)
}
```
This is just enough code to get a simple JSON API going that we can load from the `index.html`
you've been editing. To grab the data just do this:
```javascript
const [data, status] = await $get_json('/api/myfeature/user_profile');
if($no_error(data, status)) {
// use it
}
```
In place of your prototype data. Doing that will get the data from the `api.go` code you just
wrote.
### 6. Store the Data in `db.go`
Once you've sorted out how the data is going to come from your API you can start to store it in the
database. The first step for that is to move your data into `db.go`:
```go
package features_myfeature
type UserProfile struct {
UserName string
Email string
}
func GetUserProfile(id int) (UserProfile, error) {
profile := UserProfile{
UserName: "John",
Email: "zed@zed.com",
}
return profile, nil
}
```
The `UserProfile` is removed from `api.go` and placed in `db.go`. You then call your
`GetUserProfile` function to get it:
```go
import (
"github.com/gofiber/fiber/v2"
. "MY/webapp/common"
)
func GetApiUserProfile(c *fiber.Ctx) error {
profile, err := GetUserProfile(1)
if err != nil { return IfErrNil(err, c) }
return c.JSON(profile)
}
```
> __NOTE__: This is intended as a simplified way to _grow_ a feature from idea to working backend.
> Eventually you would want to create a separate module for your data models, create your tests for
> them, and other important design considerations.
### 7. Move Hard Stuff to `views.go`
There's some things that are too hard to do in JavaScript, but are very easy to do in `views.go`
usign the basic templates available. An example of this is defining the structure of a `<table>`
tag. Doing this in JavaScript is a massive pain, but doing it with a template in `views.go` is
easy. Another example would be setting the `id` for some data object the page should display. It's
far easier to do that right in the template.
I won't get into how this is done, but look at any of the examples in `features/` and `admin/` to
see me do this.
### 8. Put it In The Database
Once you have your data API worked out you can then use
[squirrel](https://github.com/Masterminds/squirrel), [goose](https://github.com/pressly/goose), and
[sqlx](https://github.com/jmoiron/sqlx) to make it store everything in the database.
First you create a migration for your data:
```shell
goose sqlite3 db.sqlite3 create create_user_profile sql -dir migrations
```
You then edit the `_create_user_profile.sql` file to create the new table:
```sql
-- +goose Up
-- +goose StatementBegin
CREATE TABLE user_profile (
username TEXT
email TEXT
);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE user_profile;
-- +goose StatementEnd
```
You use `goose` to migrate the database up:
```shell
goose sqlite3 db.sqlite3 -dir migrations up
```
If you make a mistake, you can migrate "down" to undo your last migration:
```shell
goose sqlite3 db.sqlite3 -dir migrations down
```
I also have convenient `Makefile` actions for this:
```shell
make migrate_up
make migrate_down
```
### 9. Rewrite `db.go` to Store It
The next step needs refinement, so I'll only briefly describe it now:
1. You have your data, so annotate it with `db: ""` struct tags.
2. Use the `squirrel` and `sqlx` APIs to store it.
3. Add your usual CRUD operations.
I'm going to improve this so most of this can be automated, so stay tuned. For an example of doing
this look in `admin/table/db.go`.
### 10. Refine, Refine, Refine
Finally, nothing in this process is meant to be a lock-step one and done operation. This is simply a
mechanism to help people go from abstract idea to concrete working code with all the stages
necessary. Once you have everything "working" you'll need to refine it. The key to refinement is to
think about how this feature works with other features:
1. Are you repeating data that some other feature has? Just use that feature's data operations then.
2. Are you repeating components that another feature has? Try making a `lit.js` component you both use.
3. Should you move the data used by many features into a single place? Look at `data/model.go`.
You should also go over the API and page interactions in your feature, and when it's good move onto
the next feature. The only way to learn this step is to do it a lot, but in general, less is more.
If you've got 10k lines of code in a feature then rethink it.
## Note on The Process for Pros
This process is mostly for people who are learning how to build a web application. Once you've done
it this way for a few web apps then you can do it however works best for your idea.
Some people's ideas are about the data, so starting with the UI like this may not work. If your
idea is about the data, start with the `api.go` and `db.go`, and then work the tests.
Generally though, I've found that people who start with the backend are mostly only starting there
because that's what they know. They also tend to create glorious crystal palaces that are
completely unnecessary because they aren't grounded in the reality of a UI actual people need to
use.
When you start with the UI can you can prototype all of the interactions, figure out the data, and
aim for only what you need. Keep doing this and you'll slowly build a nice data model with less
cruft for the application you're creating.
## Additional Features
## Sending Emails
> __WARNING__: The email sending feature is currently very simple. Don't think you can send 200
> million spam messages with it.
The email system uses a [Redis]() queue to schedule messages, and an email router to actually
deliver them. The idea is that you don't want to wait for email crafting and delivery in your web
application most of the time. Ideally what you want to do is:
1. In an `api.go` you receive a form or action from the user that needs an email. Say a password
reset.
2. You use the `common/email` module to craft a simple message and send it to the queue. Your
message at this stage is usually a very simple `struct` with the information your email router
needs to build the email.
3. In your `api.go` file _immediately return rather than wait for email delivery._
4. Your email router then sits there and processes the email requests. When a new message comes in
it takes the data, queries your database, configures the templates, and finally sends it to the
email server.
Since email is asynchronous there's no point in your web application waiting around for all of #4 to
be done. Just toss out a message on a queue and move on.
### Running the `mailer`
There's a simple router implemented in `tools/cmd/mailer` that you can run to get going. You can
use this to send simple emails but you should probably write a new one that handles your more
sophisticated emails like password resets and receipts.
You can run it from the command line with:
```shell
./bin/mailer
```
It will require that you install `redis` for it to work.
### Testing Your Email Locally
Running an email server locally is kind of a pain, and setting up everything so you can view sent
emails is even harder. That's way the awesome and fabulous [MailHog](https://github.com/mailhog/MailHog) exists.
You can install it easily (it's written in Go) and then run it:
```shell
MailHog
```
By default it opens an SMTP port on `1025` and a web server at 127.0.0.1:8025. If you point your
browser at 127.0.0.:8025 you'll get a fake email inbox. This inbox will show you both the HTML and
TEXT versions of your email, plus other useful things.
### How To Send
Here's an example of sending an email from a form submission in `features/email/api.go`.
```go
package features_email
import (
email "MY/webapp/common/email"
"github.com/gofiber/fiber/v2"
)
func PostApiEmailSend(c *fiber.Ctx) error {
go email.OneShotSend(email.EmailMessage{
To: c.FormValue("To"),
From: c.FormValue("From"),
Subject: c.FormValue("Subject"),
Template: c.FormValue("Template"),
})
return c.Redirect("/email/")
}
func SetupApi(app *fiber.App) {
app.Post("/api/email/send", PostApiEmailSend)
}
```
### Email Templates
The templates use the same default `text/template` engine that the rest of the system uses, but you
should use Markdown to write them. This automatically gives you both a nice text format (Markdown
already is like an email) and an HTML output. Look at `emails/signup.md` for a simple example.
## Receiving Payments
Coming soon...
## FAQ
Here's some questions I get when people see this.
### What if My Email Templates Need HTML?
You should be able to work with the code in `common/email` to make it do what you want. I don't
think it's specifically looking for any extension, but I haven't tested it. Try just dropping the
`.html` you want into the `emails/` directory and sending it.
You should also know that really complex HTML usually get filtered, so it's best to keep it simple
HTML and Markdown will help you stick to that.
### Isn't Fiber Banned?!
First off, nobody has the right to "ban" a project. If you're hanging out in the Go discord and
you've seen Fiber on some ban list then whoever did that is a fucking asshole. Just ignore that
bullshit and use the best tech you can.
Second, if you're only supposed to use the Go standard library then why [is there a whole fucking
document on the official go website explaining how to use Gin to make an API?](https://go.dev/doc/tutorial/web-service-gin). It seems like whoever is claiming that you are only allowed to use the Go standard library is in disagreement with the _actual fucking Go project_.
Finally, you don't have to do what you're told, especially when the person telling you what to do is
some random asshole hiding in a chat room being a random asshole. Use what you want, and Fiber is
in that sweet spot of being a nice API that isn't slow dogshit like [Gin](https://www.techempower.com/benchmarks/).
### Why Not Gin?
Gin is a piece of crap, that's why. Look at the [Techempower Benchmarks](https://www.techempower.com/benchmarks/) to get an idea of how bad Gin is. It's at the _bottom_ of the list, performing even _worse_ that some of the slowest _Ruby_ web frameworks out there. Make a _compiled language_ like Go perform that bad takes a special kind of incompetence.
### Why Not Only the Go Standard Library?
Because I like nice things, and while the Go Standard Library has everything you _need_, it
frequently is missing things you _want_. This project is about adding the things you'll probably
want without getting to abstract and far away from learning the core web technologies.
### Are Your Tags "Accessible"?! What about SEO?!
Yes, I actually tested all of this with [NV Access](https://www.nvaccess.org/) and the only thing NV
Access seems to have a problem with is the `pre` and `code` tags. That's because apparently those
are deemed as "images" so they get ignored...which is about the dumbest fucking shit I've ever
heard. I'm working on a way to fix that.
Anyone telling you that my `grid`, `bar`, `block`, or `stack` tags impact accessibility most likely
has never ran NV Access one time so they're just wrong. Additionally, it's better to have these be
_ignored_ by screen readers because they're just 2D layout junk, so they're kind of irrelevant to a
linear 1D screen reader.
Finally, SEO isn't impacted because Google has literally said HTML quality doesn't matter for SEO
ranking. Think about it, if Google went around dictating exact HTML they would get in so much
Monopoly trouble. Instead they tend to focus more on content and organization, and I believe the
official quote is, "Making everyone use the same HTML would make the web boring."
However, take this with a grain of salt because the same Google people who've said this also said
that backlinks didn't impact performance and many SEO experts say this is totally not true. Do your
own testing, and if it impacts your SEO than just change them after you get your initial design up.

@ -0,0 +1,3 @@
* The qmgr needs to actually manage the queue, currently it's just a little testing tool.
* Probably also need an email testing tool that can send sample emails off the cli.
* Try to remove code.js and either bring the functionality to jzed.js or not use it.

@ -0,0 +1,75 @@
package common
import (
"log"
"reflect"
"github.com/gofiber/fiber/v2"
"github.com/go-playground/validator/v10"
)
type Failure struct {
Message string `json:"message"`
Status int
}
func IfErrNil(err error, c *fiber.Ctx) error {
if err != nil {
log.Output(10, err.Error())
status := fiber.StatusInternalServerError
c.SendStatus(status)
return c.JSON(Failure{err.Error(), status})
} else {
return err
}
}
func ReceivePost[T any](c *fiber.Ctx) (*T, error) {
var result *T
result = new(T)
if err := c.BodyParser(result); err != nil {
log.Println(err);
return result, err
}
var validate *validator.Validate
validate = validator.New(validator.WithRequiredStructEnabled())
if err := validate.Struct(result); err != nil {
validationErrors := err.(validator.ValidationErrors)
for _, validerr := range validationErrors {
log.Println(validerr.Field(), validerr.Error())
}
return result, err
}
return result, nil
}
func ReflectOnPost(typeOf reflect.Type, c *fiber.Ctx) (reflect.Value, error) {
var result_val reflect.Value
result_val = reflect.New(typeOf)
result := result_val.Interface()
if err := c.BodyParser(result)
err != nil {
log.Println(err);
return result_val, err
}
var validate *validator.Validate
validate = validator.New(validator.WithRequiredStructEnabled())
err := validate.Struct(result)
if err != nil {
validationErrors := err.(validator.ValidationErrors)
log.Println(validationErrors)
return result_val, err
}
return result_val, nil
}

@ -0,0 +1,39 @@
package common
import (
"errors"
"log"
"github.com/gofiber/fiber/v2"
_ "github.com/mattn/go-sqlite3"
"github.com/gofiber/fiber/v2/middleware/session"
"MY/webapp/data"
"MY/webapp/config"
)
func IsAdmin(user *data.User) bool {
return user.Username == config.Settings.Server.Admin
}
func AuthCheck(c *fiber.Ctx, needs_admin bool) (*session.Session, error) {
sess, err := STORE.Get(c)
if err != nil { return sess, err }
// BUG: this has to come from the databse, just temporary
admin := sess.Get("admin") == true
authed := sess.Get("authenticated") == true
if needs_admin {
authed = admin && authed
log.Printf("after needs_admin block: authed=%v", authed)
}
if authed {
log.Println("user is authed, return nil and sess")
return sess, nil
} else {
log.Println("user is NOT authed, return error")
return sess, errors.New("Authentication, permission failure")
}
}

@ -0,0 +1,167 @@
package common_email
import (
"github.com/wneessen/go-mail"
"encoding/json"
"context"
"log"
"github.com/redis/go-redis/v9"
"github.com/yuin/goldmark"
"MY/webapp/config"
"path/filepath"
"text/template"
"os"
"strings"
)
func NewSender(ctx context.Context) (Sender){
client := redis.NewClient(&redis.Options{
Addr: config.Settings.Redis.HostPort,
Password: config.Settings.Redis.Password,
DB: config.Settings.Redis.DB,
})
return Sender{client, ctx}
}
func NewRouter(ctx context.Context) (Router, error) {
var router Router
var err error
router.ctx = ctx
router.redis_client = redis.NewClient(&redis.Options{
Addr: config.Settings.Redis.HostPort,
Password: config.Settings.Redis.Password,
DB: config.Settings.Redis.DB,
})
router.smtp_client, err = mail.NewClient(config.Settings.Email.Host,
mail.WithPort(config.Settings.Email.Port),
mail.WithTLSPolicy(mail.NoTLS))
return router, err
}
func (router *Router) Close() {
router.redis_client.Close()
router.smtp_client.Close()
}
func (sender *Sender) Close() {
sender.redis_client.Close()
}
func (router *Router) Render(tmpl_path string, data any) (string, string, error) {
tmpl_path = filepath.Join(config.Settings.Email.Templates, tmpl_path)
// load the markdown file
md_file, err := os.ReadFile(tmpl_path)
if err != nil { return "", "", err }
// template it
tmpl, err := template.New(tmpl_path).Parse(string(md_file))
if err != nil { return "", "", err }
out := new(strings.Builder)
err = tmpl.Execute(out, data)
if err != nil { return "", "", err }
text_out := out.String()
// convert to html
out.Reset()
err = goldmark.Convert([]byte(text_out), out)
if err != nil { return "", "", err }
html_out := out.String()
// return the template .md as plain text and .html as html
return string(text_out), string(html_out), nil
}
func (router *Router) DeliverEmail(msg EmailMessage) error {
email_msg := mail.NewMsg()
err := email_msg.From(msg.From)
if err != nil { return err }
err = email_msg.To(msg.To)
if err != nil { return err }
email_msg.Subject(msg.Subject)
if config.Settings.Email.XMailer != "" {
email_msg.SetGenHeader(mail.HeaderXMailer, config.Settings.Email.XMailer)
}
if config.Settings.Email.UserAgent != "" {
email_msg.SetGenHeader(mail.HeaderUserAgent, config.Settings.Email.UserAgent)
}
log.Println("template is", msg.Template)
text, html, err := router.Render(msg.Template, msg)
if err != nil { return err }
email_msg.SetBodyString(mail.TypeTextHTML, html)
email_msg.AddAlternativeString(mail.TypeTextPlain, text)
err = router.smtp_client.DialAndSendWithContext(router.ctx, email_msg)
if err != nil { return err }
return nil
}
func (router *Router) ReceiveEmail() (EmailMessage, error) {
var msg EmailMessage
result, err := router.redis_client.BRPop(
router.ctx, 0,
config.Settings.Email.RedisQueue).Result()
if err != nil { return msg, err }
// NOTE: 0=queue name, 1=message
err = json.Unmarshal([]byte(result[1]), &msg)
if err != nil { return msg, err }
return msg, nil
}
func (router *Router) HandleEmailRequest() error {
msg, err := router.ReceiveEmail()
if err != nil { return err }
err = router.DeliverEmail(msg)
if err != nil { return err }
return nil
}
func (sender *Sender) QueueEmail(msg EmailMessage) error {
log.Println("email msg", msg)
msg_json, err := json.Marshal(msg)
if err != nil { return err }
_, err = sender.redis_client.LPush(
sender.ctx,
config.Settings.Email.RedisQueue,
string(msg_json)).Result()
if err != nil { return err }
return nil
}
// Intended to be fired off on a goroutine and
// forgotten about.
func OneShotSend(msg EmailMessage) {
ctx := context.Background()
sender := NewSender(ctx)
defer sender.Close()
err := sender.QueueEmail(msg)
if err != nil {
log.Print(err)
}
}

@ -0,0 +1,28 @@
package common_email
import (
"github.com/redis/go-redis/v9"
"github.com/wneessen/go-mail"
"context"
)
type EmailMessage struct {
To string
From string
Subject string
Template string
Data any
}
type Sender struct {
redis_client *redis.Client
ctx context.Context
}
type Router struct {
redis_client *redis.Client
smtp_client *mail.Client
ctx context.Context
}

@ -0,0 +1,29 @@
package common
import (
"log"
"fmt"
"github.com/gofiber/fiber/v2"
)
func Fail(err error, format string, v ...any) error {
err_format := fmt.Sprintf("ERROR: %v; %s", err, format)
log.Printf(err_format, v...)
return err
}
func ApiError(c *fiber.Ctx, format string, args ...any) error {
c.Status(500)
log.Printf(format, args...)
return c.JSON(fiber.Map{
"error": fmt.Sprintf(format, args...),
})
}
func RenderError(c *fiber.Ctx, format string, args ...any) error {
return c.Render("error", fiber.Map{
"Error": fmt.Sprintf(format, args...),
})
}

@ -0,0 +1,108 @@
package common
import (
"strings"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/session"
"io/fs"
"path/filepath"
"fmt"
)
var STORE *session.Store
func GenPageId(path string) string {
return strings.ReplaceAll(path, "/", "-") + "-page"
}
func AddAuthedPage(app *fiber.App, must_admin bool, url string, view string) {
handler := func(c *fiber.Ctx) error {
_, err := AuthCheck(c, must_admin)
if err != nil { return c.Redirect("/") }
params := c.AllParams()
params["PageId"] = GenPageId(url)
return c.Render(view, params)
}
app.Get(url, handler)
}
func AddPage(app *fiber.App, url string, view string) {
handler := func(c *fiber.Ctx) error {
params := c.AllParams()
params["PageId"] = GenPageId(url)
return c.Render(view, c.AllParams())
}
app.Get(url, handler)
}
func Page(path string) (func(c *fiber.Ctx) error) {
page_id := GenPageId(path)
return func (c *fiber.Ctx) error {
return c.Render(path, fiber.Map{"PageId": page_id})
}
}
func SplitRouteTemplate(path string) (string, string) {
path = filepath.ToSlash(filepath.Clean(path))
path, found := strings.CutPrefix(path, "views/")
if !found {
fmt.Println("no views/ prefix?", path)
}
ext := filepath.Ext(path)
source_name, _ := strings.CutSuffix(path, ext)
split_path := strings.Split(source_name, "/")
var route string
page := strings.Join(split_path, "/")
last := split_path[len(split_path) - 1]
if last == "index" {
// it's an index.html so route doesn't include it
// WARN: page already has the leading /
route = fmt.Sprintf("/%s/", filepath.Dir(page))
} else {
route = fmt.Sprintf("/%s", page)
}
return route, page
}
func ConfigViews(app *fiber.App, path string) error {
route_map := make(map[string]string, 10)
for _, route := range app.GetRoutes() {
if route.Method == "GET" {
route_map[route.Path] = route.Method
}
}
err := filepath.WalkDir(path,
func(path string, d fs.DirEntry, err error) error {
if err != nil { return err }
if !d.IsDir() && strings.HasSuffix(path, "html") {
route, page := SplitRouteTemplate(path)
_, exists := route_map[route]
if exists {
fmt.Println("ROUTE ", route, "Already exists, skipping", page)
} else {
fmt.Println("ROUTE ", route, "does not conflict page", page)
AddPage(app, route, page)
// update the route map to avoid dupes
route_map[route] = "GET"
}
}
return nil
})
return err
}

@ -0,0 +1,54 @@
package config
import (
"log"
"encoding/json"
"os"
)
type config struct {
Server struct {
Admin string
Views string
Layouts string
HostPort string
}
Database struct {
Driver string
Url string
}
Redis struct {
HostPort string
Password string
DB int
}
Email struct {
Host string
Port int
XMailer string
UserAgent string
RedisQueue string
Templates string
}
Paypal struct {
ClientID string
SecretID string
URL string
}
}
var Settings config
func Load(path string) {
data, err := os.ReadFile(path)
if err != nil {
log.Fatalf("error loading %s: %v", path, err)
}
err = json.Unmarshal(data, &Settings)
if err != nil {
log.Fatalf("error parsing %s: %v", path, err)
}
}

@ -0,0 +1,29 @@
{
"Server": {
"Admin": "admin",
"Views": "./views",
"Layouts": "layouts/main",
"HostPort": "127.0.0.1:7001"
},
"Database": {
"Driver": "sqlite3",
"Url": "db.sqlite3"
},
"Redis": {
"HostPort": "localhost:6379",
"Password": "",
"DB": 0
},
"Email": {
"Host": "localhost",
"Port": 1025,
"XMailer": "nunya-business",
"UserAgent": "nunya-business",
"RedisQueue": "email",
"Templates": "emails"
},
"Paypal": {
"ClientID": "test-client-id",
"SecretID": "test-secret"
}
}

@ -0,0 +1,59 @@
package data
import (
"log"
"database/sql"
"github.com/gofiber/fiber/v2"
_ "github.com/mattn/go-sqlite3"
"github.com/jmoiron/sqlx"
)
var configured bool
var DB *sqlx.DB
func Setup(driver string, url string) {
if(!configured) {
var err error
DB, err = sqlx.Connect(driver, url)
if err != nil {
log.Fatalln(err)
}
configured = true;
}
}
func Shutdown() {
DB.Close()
}
func SelectJson[T any](c *fiber.Ctx, err error, sql string, args ...interface{}) error {
// BUG: add a precondition that aborts if array of interfaces to avoid "unsupported $1 array of interfaces"
var result []T
if err != nil { goto fail }
err = DB.Select(&result, sql, args...)
if err != nil { goto fail }
return c.JSON(&result)
fail: return err
}
func GetJson[T any](c *fiber.Ctx, err error, sql string, args ...interface{}) error {
var result T
if err != nil { goto fail }
err = DB.Get(&result, sql, args...)
if err != nil { goto fail }
return c.JSON(&result)
fail: return err
}
func Exec(err error, sql_query string, args ...interface{}) (sql.Result, error) {
if err != nil { return nil, err }
result, err := DB.Exec(sql_query, args...)
return result, err
}

@ -0,0 +1,56 @@
package data
import (
"reflect"
"github.com/guregu/null/v6"
)
type Login struct {
Username string `db:"username" validate:"required,max=30"`
Password string `db:"password" validate:"required,max=128"`
}
type User struct {
Id int `db:"id" validate:"numeric"`
Username string `db:"username" validate:"required,max=30"`
Email string `db:"email" validate:"required,email,max=128"`
Password string `db:"password" validate:"required,min=8,max=64"`
}
/*
* Example of using the null library to do optional fields.
*/
type NullExample struct {
Id int `db:"id" validate:"numeric"`
HasMaybe null.Int `db:"replying_to" validate:"omitempty,numeric"`
}
type Product struct {
Id int `db:"id" validate:"numeric"`
Title string `db:"title" validate:"required"`
Description string `db:"description" validate:"required"`
Price float64 `db:"price" validate:"required"`
Slug string `db:"slug" validate:"required"`
}
type CartItem struct {
Id int `db:"id" validate:"numeric"`
UserId int `db:"user_id" validate:"numeric,required"`
ProductId int `db:"product_id" validate:"required"`
Quantity int `db:"quantity" validate:"numeric"`
}
type Cart struct {
Items []CartItem
}
func Models() map[string]reflect.Type {
return map[string]reflect.Type{
"user": reflect.TypeFor[User](),
"product": reflect.TypeFor[Product](),
"cart_item": reflect.TypeFor[CartItem](),
"null_example": reflect.TypeFor[NullExample](),
}
}

@ -0,0 +1,10 @@
Welcome
=======
Welcome to my website! Here's what you sent:
* {{ .From }}
* {{ .To }}
* {{ .Data.Name }}
* {{ .Data.Pet }}

@ -0,0 +1,155 @@
package features_admin
import (
"maps"
"reflect"
"fmt"
"github.com/gofiber/fiber/v2"
"MY/webapp/data"
. "MY/webapp/common"
)
func GetApiTableIndex(c *fiber.Ctx) error {
_, err := AuthCheck(c, true)
if err != nil { return c.Redirect("/") }
var tables []string
for k := range maps.Keys(data.Models()) {
tables = append(tables, k)
}
return c.JSON(tables)
}
func GetApiSelectAll(c *fiber.Ctx) error {
_, err := AuthCheck(c, true)
if err != nil { return c.Redirect("/") }
table := c.Params("table")
if table == "" {
return c.Redirect("/admin/table/")
}
page := c.QueryInt("page", 0)
if page < 0 { page = 0 }
search := c.Query("search", "")
var result []any
if search == "" {
result, err = SelectTable(table, 20, uint64(page));
if err != nil { return IfErrNil(err, c) }
} else {
// NOTE: need a 404 here when there's no result? or empty list?
result, err = SearchTable(search, table, 20, uint64(page));
if err != nil { return IfErrNil(err, c) }
}
return c.JSON(result)
}
func GetApiSelectOne(c *fiber.Ctx) error {
_, err := AuthCheck(c, true)
if err != nil { return c.Redirect("/") }
table := c.Params("table")
id, err := c.ParamsInt("id", -1)
if err != nil || id < 0 { return IfErrNil(err, c) }
result, err := Get(table, int64(id))
if err != nil { return IfErrNil(err, c) }
return c.JSON(result.Interface())
}
func PostApiUpdate(c *fiber.Ctx) error {
_, err := AuthCheck(c, true)
if err != nil {
return ApiError(c, "Auth required.")
}
table := c.Params("table")
typeOf, ok := data.Models()[table]
if !ok {
return ApiError(c, "Table does not exist")
}
obj, err := ReflectOnPost(typeOf, c)
if err != nil {
return ApiError(c, fmt.Sprintf("Invalid format: %v", err))
}
id, err := Update(table, obj.Elem())
if err != nil {
return ApiError(c, "Update failed")
}
return c.JSON(fiber.Map{"table": table, "id": id})
}
func GetApiInsert(c *fiber.Ctx) error {
_, err := AuthCheck(c, true)
if err != nil { return c.Redirect("/") }
table := c.Params("table")
// check that this table exists in the model
typeOf, ok := data.Models()[table]
if !ok {
return ApiError(c, "admin table %s does not exist", table)
}
result := reflect.New(typeOf)
return c.JSON(result.Interface())
}
func PostApiInsert(c *fiber.Ctx) error {
_, err := AuthCheck(c, true)
if err != nil {
return ApiError(c, "login required")
}
table := c.Params("table")
typeOf := data.Models()[table]
obj, err := ReflectOnPost(typeOf, c)
if err != nil {
return ApiError(c, "failed reflect")
}
id, _, err := Insert(table, obj.Elem())
if err != nil {
return ApiError(c, "failed insert")
}
return c.JSON(fiber.Map{ "id": id, "table": table})
}
func DeleteApi(c *fiber.Ctx) error {
_, err := AuthCheck(c, true)
if err != nil { return c.Redirect("/") }
table := c.Params("table")
id, err := c.ParamsInt("id", -1)
if err != nil || id < 0 { return IfErrNil(err, c) }
err = Delete(table, int64(id))
if err != nil { return IfErrNil(err, c) }
return c.JSON(fiber.Map{})
}
func SetupApi(app *fiber.App) {
app.Get("/api/admin/table", GetApiTableIndex)
app.Get("/api/admin/table/:table", GetApiSelectAll)
app.Get("/api/admin/table/new/:table", GetApiInsert)
app.Post("/api/admin/table/new/:table", PostApiInsert)
app.Get("/api/admin/table/:table/:id", GetApiSelectOne)
app.Post("/api/admin/table/:table/:id", PostApiUpdate)
app.Delete("/api/admin/table/:table/:id", DeleteApi)
}

@ -0,0 +1,11 @@
package features_admin
import (
"github.com/gofiber/fiber/v2"
)
func Setup(app *fiber.App) {
SetupApi(app)
SetupPages(app)
}

@ -0,0 +1,51 @@
package features_admin
import (
"github.com/gofiber/fiber/v2"
. "MY/webapp/common"
)
func GetPageSelectOne(c *fiber.Ctx) error {
_, err := AuthCheck(c, true)
if err != nil { return c.Redirect("/") }
table := c.Params("table")
id, err := c.ParamsInt("id", -1)
if err != nil || id < 0 {
// make a way to return an error here
return RenderError(c, "Invalid id %d", id)
}
return c.Render("admin/table/view", fiber.Map{
"table": table,
"id": id,
})
}
func GetPageContent(c *fiber.Ctx) error {
_, err := AuthCheck(c, true)
if err != nil { return c.Redirect("/") }
table := c.Params("table")
headers, err := Schema(table)
if err != nil {
return ApiError(c, "Invalid table")
}
return c.Render("admin/table/contents", fiber.Map{
"table": table,
"headers": headers,
})
}
func SetupPages(app *fiber.App) {
AddAuthedPage(app, true, "admin/table/", "admin/table/index")
AddAuthedPage(app, true, "admin/table/new/:table/", "admin/table/new")
AddAuthedPage(app, true, "admin/", "admin/index")
app.Get("admin/table/:table/", GetPageContent)
app.Get("/admin/table/:table/:id/", GetPageSelectOne)
}

@ -0,0 +1,80 @@
package features_auth
import (
"github.com/gofiber/fiber/v2"
_ "github.com/mattn/go-sqlite3"
sq "github.com/Masterminds/squirrel"
"log"
"MY/webapp/data"
. "MY/webapp/common"
)
func GetApiAuthCheck(c *fiber.Ctx) error {
_, err := AuthCheck(c, false)
// auth failure or not authed is determined by err, with nil meaning YES AUTHED
return c.JSON(fiber.Map{"is_authed": err == nil})
}
func GetApiLogout(c *fiber.Ctx) error {
err := LogoutUser(c)
if err != nil { return IfErrNil(err, c) }
return c.Redirect("/")
}
func PostApiRegister(c *fiber.Ctx) error {
user, err := ReceivePost[data.User](c)
if err != nil { return IfErrNil(err, c) }
err = SetUserPassword(user)
if err != nil { return IfErrNil(err, c) }
sql, args, err := sq.Insert("user").
Columns("username", "email", "password").
Values(user.Username, user.Email, user.Password).ToSql()
_, err = data.Exec(err, sql, args...)
if err != nil { return IfErrNil(err, c) }
return c.Redirect("/login/")
}
func PostApiLogin(c *fiber.Ctx) error {
var user data.User
login, err := ReceivePost[data.Login](c)
if(err != nil) {
return ApiError(c, "Invalid login form.")
}
pass_good, err := LoginUser(&user, login)
if pass_good {
sess, err := STORE.Get(c)
if err != nil { return IfErrNil(err, c) }
sess.Set("user_id", user.Id)
sess.Set("authenticated", true)
sess.Set("admin", IsAdmin(&user))
err = sess.Save()
if err != nil {
return ApiError(c, "Server Failure in Session.")
}
return c.JSON(fiber.Map{"valid": true})
} else {
// custom auth error
if(err != nil) { log.Printf("Error in login: %v", err) }
c.Status(401)
return c.JSON(fiber.Map{"error": "Invalid login."})
}
}
func SetupApi(app *fiber.App) {
app.Get("/api/authcheck", GetApiAuthCheck)
app.Get("/api/logout", GetApiLogout)
app.Post("/api/register", PostApiRegister)
app.Post("/api/login", PostApiLogin)
}

@ -0,0 +1,9 @@
package features_auth
import (
"github.com/gofiber/fiber/v2"
)
func Setup(app *fiber.App) {
SetupApi(app)
}

@ -0,0 +1,22 @@
package features_email
import (
email "MY/webapp/common/email"
"github.com/gofiber/fiber/v2"
)
func PostApiEmailSend(c *fiber.Ctx) error {
go email.OneShotSend(email.EmailMessage{
To: c.FormValue("To"),
From: c.FormValue("From"),
Subject: c.FormValue("Subject"),
Template: c.FormValue("Template"),
})
return c.Redirect("/email/")
}
func SetupApi(app *fiber.App) {
app.Post("/api/email/send", PostApiEmailSend)
}

@ -0,0 +1,10 @@
package features_email
import (
"github.com/gofiber/fiber/v2"
)
func Setup(app *fiber.App) {
SetupApi(app)
SetupViews(app)
}

@ -0,0 +1,11 @@
package features_email
import (
"github.com/gofiber/fiber/v2"
. "MY/webapp/common"
)
func SetupViews(app *fiber.App) {
err := ConfigViews(app, "views/email")
if err != nil { panic(err) }
}

@ -0,0 +1,13 @@
package features_fakepay
import (
"github.com/gofiber/fiber/v2"
)
func PostApiPay(c *fiber.Ctx) error {
return c.Redirect("/fakepay/complete")
}
func SetupApi(app *fiber.App) {
app.Post("/api/fakepay/pay", PostApiPay)
}

@ -0,0 +1,10 @@
package features_fakepay
import (
"github.com/gofiber/fiber/v2"
)
func Setup(app *fiber.App) {
SetupApi(app)
SetupViews(app)
}

@ -0,0 +1,11 @@
package features_fakepay
import (
"github.com/gofiber/fiber/v2"
. "MY/webapp/common"
)
func SetupViews(app *fiber.App) {
err := ConfigViews(app, "views/fakepay")
if err != nil { panic(err) }
}

@ -0,0 +1,22 @@
package features
import (
"github.com/gofiber/fiber/v2"
"MY/webapp/features/email"
"MY/webapp/features/paypal"
"MY/webapp/features/shopping"
"MY/webapp/features/fakepay"
"MY/webapp/features/admin"
"MY/webapp/features/auth"
"MY/webapp/features/survey"
)
func Setup(app *fiber.App) {
features_auth.Setup(app)
features_admin.Setup(app)
features_email.Setup(app)
features_paypal.Setup(app)
features_shopping.Setup(app)
features_fakepay.Setup(app)
features_survey.Setup(app)
}

@ -0,0 +1,78 @@
package features_paypal
import (
"github.com/gofiber/fiber/v2"
"github.com/plutov/paypal/v4"
"os"
"context"
. "MY/webapp/common"
config "MY/webapp/config"
"fmt"
)
func CreatePaypal() (*paypal.Client, error) {
return paypal.NewClient(
config.Settings.Paypal.ClientID,
config.Settings.Paypal.SecretID,
config.Settings.Paypal.URL) // or paypal.APIBaseLive
}
func PostApiOrder(c *fiber.Ctx) error {
pay, err := CreatePaypal()
if err != nil { return IfErrNil(err, c) }
pay.SetLog(os.Stdout)
units := []paypal.PurchaseUnitRequest{
{
ReferenceID: "myinternalid1",
Amount: &paypal.PurchaseUnitAmount{
Currency: "USD",
Value: "10.99",
},
Description: "Product description",
Items: []paypal.Item{
{
Name: "Learn Go the Hard Way",
UnitAmount: &paypal.Money{
Currency: "USD",
Value: "10.99",
},
Quantity: "1",
},
},
},
}
source := &paypal.PaymentSource{}
appCtx := &paypal.ApplicationContext{}
order, err := pay.CreateOrder(context.TODO(), paypal.OrderIntentCapture, units, source, appCtx)
fmt.Println("ORDER", order)
return c.JSON(order)
}
func PostApiOrderCapture(c *fiber.Ctx) error {
orderID := c.Params("orderID")
fmt.Println("POST ORDER CAPTURE", orderID)
pay, err := CreatePaypal()
if err != nil { return IfErrNil(err, c) }
capture, err := pay.CaptureOrder(context.TODO(), orderID, paypal.CaptureOrderRequest{})
if err != nil { return IfErrNil(err, c) }
fmt.Println("CAPTURE", capture)
return c.JSON(fiber.Map{"status": "ok"})
}
func SetupApi(app *fiber.App) {
app.Post("/api/paypal/order", PostApiOrder)
app.Post("/api/paypal/order/:orderID/capture", PostApiOrderCapture)
}

@ -0,0 +1,160 @@
import express from "express";
import "dotenv/config";
import {
ApiError,
CheckoutPaymentIntent,
Client,
Environment,
LogLevel,
OrdersController,
PaymentsController,
PaypalExperienceLandingPage,
PaypalExperienceUserAction,
ShippingPreference,
} from "@paypal/paypal-server-sdk";
import bodyParser from "body-parser";
const app = express();
app.use(bodyParser.json());
const {
PAYPAL_CLIENT_ID,
PAYPAL_CLIENT_SECRET,
PORT = 8080,
} = process.env;
const client = new Client({
clientCredentialsAuthCredentials: {
oAuthClientId: PAYPAL_CLIENT_ID,
oAuthClientSecret: PAYPAL_CLIENT_SECRET,
},
timeout: 0,
environment: Environment.Sandbox,
logging: {
logLevel: LogLevel.Info,
logRequest: { logBody: true },
logResponse: { logHeaders: true },
},
});
const ordersController = new OrdersController(client);
const paymentsController = new PaymentsController(client);
/**
* Create an order to start the transaction.
* @see https://developer.paypal.com/docs/api/orders/v2/#orders_create
*/
const createOrder = async (cart) => {
const collect = {
body: {
intent: "CAPTURE",
purchaseUnits: [
{
amount: {
currencyCode: "USD",
value: "100",
breakdown: {
itemTotal: {
currencyCode: "USD",
value: "100",
},
},
},
// lookup item details in `cart` from database
items: [
{
name: "T-Shirt",
unitAmount: {
currencyCode: "USD",
value: "100",
},
quantity: "1",
description: "Super Fresh Shirt",
sku: "sku01",
},
],
},
],
},
prefer: "return=minimal",
};
try {
const { body, ...httpResponse } = await ordersController.createOrder(
collect
);
// Get more response info...
// const { statusCode, headers } = httpResponse;
return {
jsonResponse: JSON.parse(body),
httpStatusCode: httpResponse.statusCode,
};
} catch (error) {
if (error instanceof ApiError) {
// const { statusCode, headers } = error;
throw new Error(error.message);
}
}
};
// createOrder route
app.post("/api/orders", async (req, res) => {
try {
// use the cart information passed from the front-end to calculate the order amount detals
const { cart } = req.body;
const { jsonResponse, httpStatusCode } = await createOrder(cart);
res.status(httpStatusCode).json(jsonResponse);
} catch (error) {
console.error("Failed to create order:", error);
res.status(500).json({ error: "Failed to create order." });
}
});
/**
* Capture payment for the created order to complete the transaction.
* @see https://developer.paypal.com/docs/api/orders/v2/#orders_capture
*/
const captureOrder = async (orderID) => {
const collect = {
id: orderID,
prefer: "return=minimal",
};
try {
const { body, ...httpResponse } = await ordersController.captureOrder(
collect
);
// Get more response info...
// const { statusCode, headers } = httpResponse;
return {
jsonResponse: JSON.parse(body),
httpStatusCode: httpResponse.statusCode,
};
} catch (error) {
if (error instanceof ApiError) {
// const { statusCode, headers } = error;
throw new Error(error.message);
}
}
};
// captureOrder route
app.post("/api/orders/:orderID/capture", async (req, res) => {
try {
const { orderID } = req.params;
const { jsonResponse, httpStatusCode } = await captureOrder(orderID);
res.status(httpStatusCode).json(jsonResponse);
} catch (error) {
console.error("Failed to create order:", error);
res.status(500).json({ error: "Failed to capture order." });
}
});
app.listen(PORT, () => {
console.log(`Node server listening at http://localhost:${PORT}/`);
});

@ -0,0 +1,10 @@
package features_paypal
import (
"github.com/gofiber/fiber/v2"
)
func Setup(app *fiber.App) {
SetupApi(app)
SetupViews(app)
}

@ -0,0 +1,11 @@
package features_paypal
import (
"github.com/gofiber/fiber/v2"
. "MY/webapp/common"
)
func SetupViews(app *fiber.App) {
err := ConfigViews(app, "views/paypal")
if err != nil { panic(err) }
}

@ -0,0 +1,31 @@
package features_shopping
import (
"github.com/gofiber/fiber/v2"
"MY/webapp/data"
_ "github.com/mattn/go-sqlite3"
sq "github.com/Masterminds/squirrel"
"fmt"
)
func GetApiProducts(c *fiber.Ctx) error {
sql, args, err := sq.Select("*").From("Product").ToSql()
fmt.Println("SQL:", sql, args, err)
return data.SelectJson[data.Product](c, err, sql, args...)
}
func GetApiCart(c *fiber.Ctx) error {
return c.JSON(fiber.Map{})
}
func GetApiCartRemove(c *fiber.Ctx) error {
return c.Redirect("/shopping/checkout")
}
func SetupApi(app *fiber.App) {
app.Get("/api/shopping/products", GetApiProducts)
app.Get("/api/shopping/cart", GetApiCart)
app.Get("/api/shopping/cart/remove/:item_id", GetApiCartRemove)
}

@ -0,0 +1,10 @@
package features_shopping
import (
"github.com/gofiber/fiber/v2"
)
func Setup(app *fiber.App) {
SetupApi(app)
SetupViews(app)
}

@ -0,0 +1,11 @@
package features_shopping
import (
"github.com/gofiber/fiber/v2"
. "MY/webapp/common"
)
func SetupViews(app *fiber.App) {
err := ConfigViews(app, "views/shopping")
if err != nil { panic(err) }
}

@ -0,0 +1,15 @@
package features_survey
import (
"github.com/gofiber/fiber/v2"
)
func GetApiExample(c *fiber.Ctx) error {
tables := []string{"test", "that"}
return c.JSON(tables)
}
func SetupApi(app *fiber.App) {
app.Get("/api/survey/example", GetApiExample)
}

@ -0,0 +1,10 @@
package features_survey
import (
"github.com/gofiber/fiber/v2"
)
func Setup(app *fiber.App) {
SetupApi(app)
SetupViews(app)
}

@ -0,0 +1,11 @@
package features_survey
import (
"github.com/gofiber/fiber/v2"
. "MY/webapp/common"
)
func SetupViews(app *fiber.App) {
err := ConfigViews(app, "views/survey")
if err != nil { panic(err) }
}

@ -0,0 +1,64 @@
module MY/webapp
go 1.26
require (
github.com/Masterminds/squirrel v1.5.4
github.com/chromedp/chromedp v0.16.0
github.com/go-playground/validator/v10 v10.30.3
github.com/gofiber/fiber/v2 v2.52.14
github.com/gofiber/template/html/v2 v2.1.3
github.com/guregu/null/v6 v6.0.0
github.com/jmoiron/sqlx v1.4.0
github.com/mattn/go-sqlite3 v1.14.49
github.com/plutov/paypal/v4 v4.17.0
github.com/redis/go-redis/v9 v9.21.0
github.com/stretchr/testify v1.11.1
github.com/wneessen/go-mail v0.8.1
github.com/yuin/goldmark v1.8.5
golang.org/x/crypto v0.54.0
)
require (
github.com/andybalholm/brotli v1.2.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chromedp/cdproto v0.0.0-20260719223732-95f6af754cfe // indirect
github.com/chromedp/sysutil v1.1.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.15 // indirect
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/gofiber/template v1.8.3 // indirect
github.com/gofiber/utils v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.19.1 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/leodido/go-urn v1.5.0 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.24 // indirect
github.com/mattn/go-runewidth v0.0.27 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.73.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.40.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lcthw.dev/go/ozai v0.1.0 // indirect
lcthw.dev/go/ssgod v0.2.2 // indirect
)
tool (
lcthw.dev/go/ozai
lcthw.dev/go/ssgod
)

137
go.sum

@ -0,0 +1,137 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/andybalholm/brotli v1.2.2 h1:HzTuoo2ErYQqf5qvcJInB8uvqSVxRttzkFexPWtnceM=
github.com/andybalholm/brotli v1.2.2/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chromedp/cdproto v0.0.0-20260719223732-95f6af754cfe h1:PmhRwLZ8qLtldQCBiydwdPFJI8WVQ936ux1cpgHLRb8=
github.com/chromedp/cdproto v0.0.0-20260719223732-95f6af754cfe/go.mod h1:RwFsSODCtFExll+GhHM6R92SARHR3Z3oipaxLHj46C0=
github.com/chromedp/chromedp v0.16.0 h1:rOO4deOm4CbZgBCa8mD9g2rDyIoNs0BkgvNrlbp5ouk=
github.com/chromedp/chromedp v0.16.0/go.mod h1:rbuGKFT1vMcFcFqKfPIO1GpX/N+2s8onm2qMxZLbU5U=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
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/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.15 h1:05iP/CYtZ/w455R/KZM6rZ5ieAdh99UPtd+d3YzLmaI=
github.com/gabriel-vasile/mimetype v1.4.15/go.mod h1:azpTcoLcDZRNgFou5j+APrqQx9HqVPWa6ijYQIIVswQ=
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 h1:KZaTBSyshWX3MP5jukJcNSuXDQTO+rNpt0J564dX/eg=
github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8=
github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/gofiber/fiber/v2 v2.52.14 h1:Of3L+9qVFaQNwPlcmEdl5IIodHz8BSE0j37R7rWu4pE=
github.com/gofiber/fiber/v2 v2.52.14/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
github.com/gofiber/template/html/v2 v2.1.3 h1:n1LYBtmr9C0V/k/3qBblXyMxV5B0o/gpb6dFLp8ea+o=
github.com/gofiber/template/html/v2 v2.1.3/go.mod h1:U5Fxgc5KpyujU9OqKzy6Kn6Qup6Tm7zdsISR+VpnHRE=
github.com/gofiber/utils v1.2.0 h1:NCaqd+Efg3khhN++eeUUTyBz+byIxAsmIjpl8kKOMIc=
github.com/gofiber/utils v1.2.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
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/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ=
github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/klauspost/compress v1.19.1 h1:VsB4HPswih7mmZ8WleSFQ75c/Ui1M4trX5oAsJnhSlk=
github.com/klauspost/compress v1.19.1/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/leodido/go-urn v1.5.0 h1:pLqT2kq1zpHW/1D18QMjMpdtX7cekxqtJJjg5ANyWw0=
github.com/leodido/go-urn v1.5.0/go.mod h1:9BORnCDhdPBJNDEX+w1bJisa8yOKYi116VeO96s4ifE=
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-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.24 h1:tGZZoVgT/KiqK1c8ocVLeDS8BSWMRd47J3Lbz7vsReI=
github.com/mattn/go-isatty v0.0.24/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/mattn/go-runewidth v0.0.27 h1:Feg/Oou5zI/wnpgDF6omIU0OokC9GxLC/WRknhVlIR0=
github.com/mattn/go-runewidth v0.0.27/go.mod h1:3qAiGCV4Koz/yuveO58qUefmUTRm8r0IGEXZ9jeHp/8=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v1.14.49 h1:B8jBHC3xhxZgxztrgruTuLucebnULQnx4W7cF7SAE9w=
github.com/mattn/go-sqlite3 v1.14.49/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/plutov/paypal/v4 v4.17.0 h1:FtqBQ/ENdSw40NsoZK2aRW9zWhMNM+TaQhH3dzDtSqg=
github.com/plutov/paypal/v4 v4.17.0/go.mod h1:4N6hzf47wQUGdS3ef94jku6kjiSvFm2hxVlT9CnIHmU=
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/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.73.0 h1:ocTOORnBWtJ+P8t/6wAjdkchMzdfHmWx2VD/DPbgZ7s=
github.com/valyala/fasthttp v1.73.0/go.mod h1:EtXQDHaR+5P18p8wqDRFpUhxr108Ga9mXvVJXHRrN2k=
github.com/wneessen/go-mail v0.8.1 h1:tVcncj02/QySVFw3zr/kXOzZcuFQqBNT6K+Rbgm/pcM=
github.com/wneessen/go-mail v0.8.1/go.mod h1:dWZ61zadzCIyvB4y1/YzC5O7MrbbzBfPkARmbosdf8w=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lcthw.dev/go/ozai v0.1.0 h1:LNuLMZV4m+ciHRjf1atcunR/63Jguvz3puEzD5vmFec=
lcthw.dev/go/ozai v0.1.0/go.mod h1:AwxRrUAb/KtUzSWgKmhv/eBg6ZjJ4tVZ9YA/DlKqtWs=
lcthw.dev/go/ssgod v0.2.2 h1:lvzhxOXDka9iejIQj7Db7ePjruBqYvqe7A5zzxsdank=
lcthw.dev/go/ssgod v0.2.2/go.mod h1:fCe4J/00a3T4pURfpUp6LZlkUeh5eHDpjApvy6HHiPk=

@ -0,0 +1,68 @@
package main
import (
"log"
"os"
"os/signal"
"syscall"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
"github.com/gofiber/template/html/v2"
"github.com/gofiber/fiber/v2/middleware/session"
_ "github.com/mattn/go-sqlite3"
recov "github.com/gofiber/fiber/v2/middleware/recover"
"MY/webapp/data"
"MY/webapp/config"
"MY/webapp/common"
"MY/webapp/features"
)
func main() {
config.Load("config.json")
log.Printf("ADMIN is %s", config.Settings.Server.Admin)
log.SetFlags(log.LstdFlags | log.Lshortfile)
engine := html.New(config.Settings.Server.Views, ".html")
engine.Reload(true)
app := fiber.New(fiber.Config{
Views: engine,
ViewsLayout: config.Settings.Server.Layouts,
CaseSensitive: true,
StrictRouting: true,
})
app.Use(logger.New())
app.Use(recov.New())
app.Static("/", "./public", fiber.Static{
Compress: false,
CacheDuration: -1, // this disables the cache
})
common.STORE = session.New()
data.Setup(config.Settings.Database.Driver, config.Settings.Database.Url)
features.Setup(app)
// this sets up graceful shutdown
go func() {
if err := app.Listen(config.Settings.Server.HostPort); err != nil {
log.Panic(err)
}
}()
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
_ = <-c
log.Println("Shutdown now...")
_ = app.Shutdown()
data.Shutdown()
log.Println("Done.")
}

@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE user (id INTEGER PRIMARY KEY, username TEXT UNIQUE NOT NULL, email TEXT UNIQUE, password TEXT NOT NULL);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE user;
-- +goose StatementEnd

@ -0,0 +1,13 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE Product (id INTEGER PRIMARY KEY,
title TEXT,
description TEXT,
price float,
slug TEXT UNIQUE);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE Product;
-- +goose StatementEnd

@ -0,0 +1,13 @@
-- +goose Up
-- +goose StatementBegin
CREATE TABLE Cart (
id INTEGER PRIMARY KEY,
user_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
quantity INTEGER);
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
DROP TABLE Cart;
-- +goose StatementEnd

@ -0,0 +1,5 @@
# About Me
My name is Zed A. Shaw. I'm the author of Learn Python the Hardway, and many other books. I started
making games in 2025 using a variety of languages. What you're seeing here is the result of this
year's work.

@ -0,0 +1,2 @@
<h1>ERROR</h1>
<p>You have an error.</p>

@ -0,0 +1,91 @@
<div class="text-gray-950">
<bar class="justify-between">
<img src="/icons/cup-soda.svg" alt="Home" />
<img src="/icons/bell.svg" alt="Home" />
<img src="/icons/menu.svg" alt="Home" />
</bar>
<block class="gap-10">
<div class="flex text-2xl gap-4">
<img src="/icons/baby.svg" />
<a href="#">go</a> / <a href="#">ssgod</a>
</div>
<bar>
<button>Unwatch</button>
<button>Start</button>
<button>Fork</button>
</bar>
</block>
<block>
<bar class="border-b-1 border-gray-500 pb-3 *:text-nowrap">
<button>Code</button>
<button>Issues</button>
<button>Pull Requests</button>
<button>Packages</button>
<button>Projects</button>
<button>Releases</button>
<button>Wiki</button>
</bar>
<block>
<p class="text-2xl">SSG is a Static Site Generator that is only a Static Site Generator. No resumes here! Just a piece of code that generates static files from templates for websites, and can do it live while you develop said templates.</p>
<h3 class="text-xl!">Manage Topics</h3>
<toolbar class="flex justify-between">
<span><img class="inline" src="/icons/timer.svg" /> 21 Commits</span>
<span><img class="inline" src="/icons/git-branch.svg" /> 1 Branch</span>
<span><img class="inline" src="/icons/tag.svg" /> 0 Tags</span>
<span><img class="inline" src="/icons/database.svg" /> 589 KiB</span>
</toolbar>
</block>
<hr class="pb-6"/>
<block class="rounded-md border-1 border-gray-300 p-0!">
<bar class="p-2! w-full justify-between border-b-1 border-gray-300">
<span><img class="inline" src="/icons/baby.svg"> Name</span>
<span>Time</span>
</bar>
<bar class="p-2! w-full justify-between border-b-1 border-gray-300">
<span><img class="inline" src="/icons/baby.svg"> Name</span>
<span>Time</span>
</bar>
<bar class="p-2! w-full justify-between border-b-1 border-gray-300">
<span><img class="inline" src="/icons/baby.svg"> Name</span>
<span>Time</span>
</bar>
<bar class="p-2! w-full justify-between border-b-1 border-gray-300">
<span><img class="inline" src="/icons/baby.svg"> Name</span>
<span>Time</span>
</bar>
<bar class="p-2! w-full justify-between border-b-1 border-gray-300">
<span><img class="inline" src="/icons/baby.svg"> Name</span>
<span>Time</span>
</bar>
<bar class="p-2! w-full justify-between border-b-1 border-gray-300">
<span><img class="inline" src="/icons/baby.svg"> Name</span>
<span>Time</span>
</bar>
<bar class="p-2! w-full justify-between border-b-1 border-gray-300">
<span><img class="inline" src="/icons/baby.svg"> Name</span>
<span>Time</span>
</bar>
<bar class="p-2! w-full justify-between border-b-1 border-gray-300">
<span><img class="inline" src="/icons/baby.svg"> Name</span>
<span>Time</span>
</bar>
<bar class="p-2! w-full justify-between border-b-1 border-gray-300">
<span><img class="inline" src="/icons/baby.svg"> Name</span>
<span>Time</span>
</bar>
<bar class="p-2! w-full justify-between border-b-1 border-gray-300">
<span><img class="inline" src="/icons/baby.svg"> Name</span>
<span>Time</span>
</bar>
<bar class="p-2! w-full justify-between border-b-1 border-gray-300">
<span><img class="inline" src="/icons/baby.svg"> Name</span>
<span>Time</span>
</bar>
</block>
</div>

@ -0,0 +1,24 @@
<style>
@import "/style.css";
</style>
<main class="flex flex-col p-4 min-h-screen bg-gray-100 text-gray-950 dark:bg-gray-900 dark:text-gray-50 gap-4! *:text-xl">
<h1 class="pb-10!">Copying Popular Websites to Learn Tailwind</h1>
<p>I've advocated for years that copying other websites is the best way to learn web design. Copying is a <b>very</b> common and established way to learn almost any creative discipline including (but not limited to): Painting, Drawing, Writing, Music, Wood Working, Lutherie, Design, and Programming. To demonstrate this I've been doing copies of websites to learn <a class="text-gray-950 bg-gray-300" href="https://tailwindcss.com/">Tailwind</a>.
</p>
<h2 class="pb-8">Current Examples</h2>
<p>Here's the ones I've done so far</p>
<ul class="p-4">
<li><a class="text-gray-950 bg-gray-300" href="/copy/gitea.html">Gitea Project Page</a></li>
<li><a class="text-gray-950 bg-gray-300" href="/copy/linkedin.html">Linkedin Mobile</a></li>
<li><a class="text-gray-950 bg-gray-300" href="/copy/stride.html">Stride Game Framework</a></li>
<li><a class="text-gray-950 bg-gray-300" href="/copy/twitch.html">Twitch Home Page</a></li>
</ul>
<h2 class="pb-8">View the Code</h2>
<p>You can view this code at <a class="text-gray-950 bg-gray-300" href="https://lcthw.dev/go/go-web-starter-kit">my git repository</a>. The copies can be found in <a class="text-gray-950 bg-gray-300" href="https://lcthw.dev/go/zedshaw-games/src/branch/main/pages/copy">pages/copy</a> and the tool I use to generate the pages is one I wrote called <a class="text-gray-950 bg-gray-300" href="https://lcthw.dev/go/ssgod">ssgod</a>.
</p>
</main>

@ -0,0 +1,97 @@
<div class="text-gray-950">
<bar class="justify-evenly border-b-1 border-gray-300">
<img src="/icons/linkedin.svg" />
<img src="/icons/search.svg" />
<img src="/icons/person-standing.svg" />
<img src="/icons/briefcase.svg" />
<img src="/icons/message-square.svg" />
<img src="/icons/bell.svg" />
</bar>
<stack>
<shape class="max-h-10">Header Image</shape>
<shape class="tiny bg-green-400! top-3 left-3 rounded-full">Avatar</shape>
</stack>
<block>
<h5 class="pb-0!">Zed Shaw</h5>
<span>I write books and stuff.</span>
<span class="font-light">Miami Beach, Florida</span>
<span class="font-bold">Shavian Publishing, LLC</span>
</block>
<shape class="w-full h-15">Show More <img class="inline" src="/icons/chevron-down.svg" /></shape>
<bar>
<shape class="tiny bg-green-400! top-3 left-3 rounded-full">Avatar</shape>
<input type="text" class="w-full rounded-xl" />
</bar>
<bar class="justify-evenly">
<span><img class="inline" src="/icons/video.svg" /> Video</span>
<span><img class="inline" src="/icons/camera.svg" /> Photo</span>
<span><img class="inline" src="/icons/file-spreadsheet.svg" /> Write article</span>
</bar>
<bar class="bg-gray-300 justify-end">
<span>Sort by: <b>Top</b></span>
</bar>
<post class="flex flex-col border-b-1 border-gray-300">
<bar class="gap-0! items-center">
<shape class="tiny max-h-15! bg-green-400! top-3 left-3 rounded-full">Avatar</shape>
<block class="gap-0!">
<span>Some Dude</span>
<span class="font-light">Title</span>
<span class="font-light">14h Edited</span>
</block>
</bar>
<block>
<p>Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro. De carne lumbering animata corpora quaeritis. Summus brains sit, morbo vel maleficia?</p>
</block>
<shape class="video">Some Image</shape>
<bar class="justify-evenly">
<span><img src="/icons/thumbs-up.svg" /></span>
<span><img src="/icons/message-square.svg" /></span>
<span><img src="/icons/repeat.svg" /></span>
<span><img src="/icons/send.svg" /></span>
</bar>
</post>
<post class="flex flex-col border-b-1 border-gray-300">
<bar class="gap-0! items-center">
<shape class="tiny max-h-15! bg-green-400! top-3 left-3 rounded-full">Avatar</shape>
<block class="gap-0!">
<span>Some Dude</span>
<span class="font-light">Title</span>
<span class="font-light">14h Edited</span>
</block>
</bar>
<block>
<p>Zombie ipsum reversus ab viral inferno, nam rick grimes malum cerebro. De carne lumbering animata corpora quaeritis. Summus brains sit, morbo vel maleficia?</p>
</block>
<shape class="video">Some Image</shape>
<bar class="justify-evenly">
<span><img src="/icons/thumbs-up.svg" /></span>
<span><img src="/icons/message-square.svg" /></span>
<span><img src="/icons/repeat.svg" /></span>
<span><img src="/icons/send.svg" /></span>
</bar>
</post>
<grid class="grid-cols-2">
<block>
<span>Job search smarter</span>
<span class="font-light">Zed reactivate this bullshit.</span>
<span>John and millions more use</span>
<button class="bg-blue-400 rounded-full">Claim Offer</button>
</block>
<shape class="tiny bg-green-400! top-3 left-3 rounded-full">Avatar</shape>
</grid>
</div>

@ -0,0 +1,154 @@
<main class="!p-4">
<h1>Sample Components</h1>
<p>This is a list of components I've made for making websites quicker. They're just pre-styled things you commonly need to do HTML first layouts and designs.</p>
<h2>Shapes</h2>
<shape class="xs">
I'm a placeholder.
</shape>
<h2>Grids</h2>
<grid class="grid-cols-2">
<shape class="xxs">0,0</shape>
<shape class="xxs">1,0</shape>
<shape class="xxs">0,1</shape>
<shape class="xxs">1,1</shape>
</grid>
<h2>Blocks & Bars</h2>
<block>
<p>Use a block to automatically flex content vertically.</p>
<p>Like these two lines are done.</p>
<bar class="rounded-lg border-1 border-gray-300">
<shape class="xxs">Image</shape>
<p>Use a bar to flex things horizontally.</p>
</bar>
<bar class="flex-end rounded-lg border-1 border-gray-300">
<shape class="xxs">Image</shape>
<block>
<span>You can also combine them in arbitrary ways to layout your content.</span>
</block>
<grid class="grid-cols-2">
<shape class="tiny">1</shape>
<shape class="tiny">2</shape>
<shape class="tiny">3</shape>
</grid>
</bar>
</block>
<h2>Stack</h2>
<p>A stack lets you place multiple elements on top of eachother, like with layers in nearly every single layout composition system in existence for the last 500 years.</p>
<stack>
<shape class="xs">I'm on bottom</shape>
<h1 class="text-red-500">Hello</h1>
<h2 class="text-orange-500">There</h2>
</stack>
<h2>Code</h2>
<pre><code>if(var != 'string') {
console.log("test");
}
</code></pre>
<p>You should type <code>ls -l</code> to list the directory.</p>
<code>if(var != 'string')</code>
<h2>Highlight/Marking</h2>
<aside>
<mark>Default</mark>
<span>This is an aside. You use it to call out something specific.</span>
</aside>
<aside>
<mark class="info">Info</mark>
<span>This is an aside. You use it to call out something specific.</span>
</aside>
<aside>
<mark class="warning">Warning</mark>
<span>A warning aside.</span>
</aside>
<aside>
<mark class="alert">Alert</mark>
<span>An alert aside.</span>
</aside>
<p>This is some <mark>marked</mark> text.</p>
<h2>Details</h2>
<details aria-label="Sample">
<summary>Details Test</summary>
A test of the details thing.
</details>
<h2>Forms</h2>
<form>
<card>
<top>Test Form</top>
<middle>
<label for="fruit">What Fruit?</label>
<select id="fruit">
<option>Apples</option>
<option>Oranges</option>
</select>
<label for="name">Name</label>
<input id="name" placeholder="Your Name?" />
</middle>
<bottom>
<button>Submit</button>
</bottom>
</card>
</form>
<h2>Cards</h2>
<p>Cards organize information vertically.</p>
<card>
<top><shape class="video">Image</shape></top>
<middle>This shows a top large image with
some text in the middle.</middle>
<bottom><button type="button">An Action</button></bottom>
</card>
<h2>Header/Nav</h2>
<p>If you put nav inside header it "just works".</p>
<header>
<nav>
<a id="home" href="/">
<svg xmlns="http://www.w3.org/2000/svg"
width="2rem"
height="2rem"
viewBox="0 0 2rem 2rem">
<use href="/icons/home.svg#home" />
</svg>
</a>
<a id="live" href="#">Live</a>
<a id="stream" href="#">Streams</a>
<a id="game" href="#">Games</a>
<a id="register" href="#">Register</a>
<a id="login" href="#">Login</a>
</nav>
</header>
<p>That's an example of a header with a nav bar in it.</p>
</main>

@ -0,0 +1,3 @@
<div class="text-gray-950">
<h1>Hello</h1>
</div>

@ -0,0 +1,155 @@
<div class="text-gray-950">
<bar class="justify-between">
<logo><img class="inline" src="/icons/box.svg"/> STRIDE</logo>
<menu>
<img src="/icons/menu.svg"/>
</menu>
</bar>
<stack>
<shape class="xxl"></shape>
<block class="justify-end">
<h2>Stride Game Engine</h2>
<p>Stride is a free and open-source cross-platform C# game engine.</p>
<p>It is fit for both 2D and 3D games, as well as any other interactive content running on desktop and VR.</p>
<bar>
<button class="btn-alert"><img class="inline" src="/icons/download.svg"/> Download 4.2</button>
<button class="btn-primary"><img class="inline" src="/icons/alert-circle.svg" />Donate</button>
</bar>
</block>
</stack>
<block>
<panel class="gap-5 flex flex-col border-2 shadow-lg border-gray-300">
<shape class="video"></shape>
<block class="w-fit">
<h3>Bet on the future</h3>
<ul class="pb-5">
<li>Blah blah blah</li>
<li>Blah blah blah</li>
<li>Blah blah blah</li>
<li>Blah blah blah</li>
<li>Blah blah blah</li>
</ul>
<button class="btn-alert">Discover more</button>
</block>
</panel>
<hr class="pb-6"/>
<panel class="gap-5 flex flex-col border-2 shadow-lg border-gray-300">
<shape class="video"></shape>
<block class="w-fit">
<h3>Bet on the future</h3>
<ul class="pb-5">
<li>Blah blah blah</li>
<li>Blah blah blah</li>
<li>Blah blah blah</li>
<li>Blah blah blah</li>
<li>Blah blah blah</li>
</ul>
<button class="btn-alert">Discover more</button>
</block>
</panel>
<hr class="pb-6" />
<block class="bg-gray-300 shadow-lg">
<h3>Recent blog posts</h3>
<grid class="grid-cols-3">
<block class="border-1 border-gray-500">
<h5>A look inside</h5>
<date class="text-sm"><img class="inline" src="/icons/calendar.svg"> April 09, 2025</date>
<p class="pb-4 pt-4">Distant Worlds 2, the sequel to the acclaimed 4X strategy game Distant Worlds: Universe, was developed by Code Force and published by Slitherine. In this blog post, the developers answer key questions from the Stride community, offering insights into their experience using the Stride engine to bring their ambitious galaxy-spanning strategy game to life.</p>
<button class="btn-alert">Read More</button>
</block>
<block class="border-1 border-gray-500">
<h5>A look inside</h5>
<date class="text-sm"><img class="inline" src="/icons/calendar.svg"> April 09, 2025</date>
<p class="pb-4 pt-4">Distant Worlds 2, the sequel to the acclaimed 4X strategy game Distant Worlds: Universe, was developed by Code Force and published by Slitherine. In this blog post, the developers answer key questions from the Stride community, offering insights into their experience using the Stride engine to bring their ambitious galaxy-spanning strategy game to life.</p>
<button class="btn-alert">Read More</button>
</block>
<block class="border-1 border-gray-500">
<h5>A look inside</h5>
<date class="text-sm"><img class="inline" src="/icons/calendar.svg"> April 09, 2025</date>
<p class="pb-4 pt-4">Distant Worlds 2, the sequel to the acclaimed 4X strategy game Distant Worlds: Universe, was developed by Code Force and published by Slitherine. In this blog post, the developers answer key questions from the Stride community, offering insights into their experience using the Stride engine to bring their ambitious galaxy-spanning strategy game to life.</p>
<button class="btn-alert">Read More</button>
</block>
</grid>
</block>
<hr class="pb-6"/>
<block class="bg-gray-300 shadow-lg">
<h4>Featured Sponsors</h4>
<p>Thanks to our featured sponsors for supporting Stride and empowering creators with the resources they need to bring their visions to life.</p>
<grid class="grid-cols-2">
<panel class="flex flex-col border-1 border-gray-500">
<div class="w-full aspect-video bg-gray-50">&nbsp;</div>
<block>
<p class="text-center">
A visual live-programming environment that takes you from rapid prototyping to final production.
</p>
</block>
</panel>
<panel class="flex flex-col border-1 border-gray-500">
<div class="w-full aspect-video bg-gray-50">&nbsp;</div>
<block>
<p class="text-center">
A visual live-programming environment that takes you from rapid prototyping to final production.
</p>
</block>
</panel>
</grid>
</block>
</block>
<block class="bg-gray-600 text-gray-50">
<grid class="grid-cols-2">
<shape class="xxs">Content1</shape>
<shape class="xxs">Content2</shape>
<shape class="xxs">Content3</shape>
<shape class="xxs">Content4</shape>
</grid>
<div class="flex justify-center">
<img src="/icons/baby.svg" />
<img src="/icons/baby.svg" />
<img src="/icons/baby.svg" />
<img src="/icons/baby.svg" />
<img src="/icons/baby.svg" />
<img src="/icons/baby.svg" />
<img src="/icons/baby.svg" />
<img src="/icons/baby.svg" />
</div>
<div class="flex justify-evenly">
<span>Supported by the .NET Foundation</span>
<span>Website v.2.0.0.31</span>
<span>© .NET Foundation and Contributors</span>
</div>
</block>
</div>

@ -0,0 +1,203 @@
<div class="text-gray-950 bg-gray-200">
<bar class="bg-gray-400 justify-evenly items-center">
<img src="/icons/twitch.svg" />
<span>Browse</span>
<img src="/icons/menu.svg" />
<bar class="p-0! bg-gray-300 rounded-lg border-1 border-gray-700">
<input placeholder="Search" />
<img src="/icons/search.svg" />
</bar>
<img src="/icons/crown.svg" />
<button type="button">Log In</button>
<button type="button">Sign Up</button>
<img src="/icons/person-standing.svg" />
</bar>
<div class="p-1">
<bar>
<img src="/icons/chevron-left.svg" />
<shape class="video">
Main Video
</shape>
<img src="/icons/chevron-right.svg" />
</bar>
<block>
<a class="text-xl">Live on Twitch</a>
<grid class="grid-cols-2 p-0! gap-2">
<block class="p-0!">
<shape class="video">Sample Video</shape>
<div class="flex align-top gap-3">
<shape class="bg-green-400! top-3 left-3 rounded-full aspect-square! max-w-15 max-h-15">Avatar</shape>
<div class="flex flex-col gap-1">
<span class="font-bold">Title</span>
<span>Name</span>
<span>Game Title</span>
<span>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
</span>
</div>
</div>
</block>
<block class="p-0!">
<shape class="video">Sample Video</shape>
<div class="flex align-top gap-3">
<shape class="bg-green-400! top-3 left-3 rounded-full aspect-square! max-w-15 max-h-15">Avatar</shape>
<div class="flex flex-col gap-1">
<span class="font-bold">Title</span>
<span>Name</span>
<span>Game Title</span>
<span>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
</span>
</div>
</div>
</block>
</bar>
</block>
<div class="flex justify-center items-center">
<a href="#">Show more v</a>
</div>
<block>
<span><a href="#">Categories</a> we think you'll like</span>
<grid class="grid-cols-4 p-0! gap-2">
<block class="p-0!">
<shape class="aspect-[9/12]!">Category</shape>
<div class="flex flex-col gap-1">
<span class="font-bold">Title</span>
<span>Name</span>
<span>Game Title</span>
<span>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
</span>
</div>
</block>
<block class="p-0!">
<shape class="aspect-[9/12]!">Category</shape>
<div class="flex flex-col gap-1">
<span class="font-bold">Title</span>
<span>Name</span>
<span>Game Title</span>
<span>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
</span>
</div>
</block>
<block class="p-0!">
<shape class="aspect-[9/12]!">Category</shape>
<div class="flex flex-col gap-1">
<span class="font-bold">Title</span>
<span>Name</span>
<span>Game Title</span>
<span>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
</span>
</div>
</block>
<block class="p-0!">
<shape class="aspect-[9/12]!">Category</shape>
<div class="flex flex-col gap-1">
<span class="font-bold">Title</span>
<span>Name</span>
<span>Game Title</span>
<span>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
</span>
</div>
</block>
</grid>
</block>
<grid class="grid-cols-3">
<stack class="p-0!">
<button type="button" class="rounded-xxl text-left text-xxl">Games</button>
<img class="left-40 bottom-6 max-w-20 max-h-20" src="/icons/gamepad.svg" />
</stack>
<stack class="p-0!">
<button type="button" class="rounded-xxl text-left text-xxl">IRL</button>
<img class="left-40 bottom-6 max-w-20 max-h-20" src="/icons/mic.svg" />
</stack>
<stack class="p-0!">
<button type="button" class="rounded-xxl text-left text-xxl">Music</button>
<img class="left-40 bottom-6 max-w-20 max-h-20" src="/icons/headphones.svg" />
</stack>
<stack class="p-0!">
<button type="button" class="rounded-xxl text-left text-xxl">Creative</button>
<img class="left-40 bottom-6 max-w-20 max-h-20" src="/icons/paintbrush.svg" />
</stack>
<stack class="p-0!">
<button type="button" class="rounded-xxl text-left text-xxl">Esports</button>
<img class="left-40 bottom-6 max-w-20 max-h-20" src="/icons/trophy.svg" />
</stack>
</grid>
<block>
<a class="text-xl">Live on Twitch</a>
<grid class="grid-cols-2 p-0! gap-2">
<block class="p-0!">
<shape class="video">Sample Video</shape>
<div class="flex align-top gap-3">
<shape class="bg-green-400! top-3 left-3 rounded-full aspect-square! max-w-15 max-h-15">Avatar</shape>
<div class="flex flex-col gap-1">
<span class="font-bold">Title</span>
<span>Name</span>
<span>Game Title</span>
<span>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
</span>
</div>
</div>
</block>
<block class="p-0!">
<shape class="video">Sample Video</shape>
<div class="flex align-top gap-3">
<shape class="bg-green-400! top-3 left-3 rounded-full aspect-square! max-w-15 max-h-15">Avatar</shape>
<div class="flex flex-col gap-1">
<span class="font-bold">Title</span>
<span>Name</span>
<span>Game Title</span>
<span>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
<a class="bg-gray-700 text-gray-300 rounded-lg p-1" href="#">Pill</a>
</span>
</div>
</div>
</block>
</bar>
</block>
<block class="justify-center items-center">
<h4>an amazon company</h4>
</block>
</div>
</div>

@ -0,0 +1,39 @@
<h1>Zed's Survey Site</h1>
<p>La jetée raclette normcore, ascot comme des garçons hyperpop canon event supper club reading series leggings burrata. Canon event listening party carhartt bodega boys black trumpet tarkovsky. Narwhal portland fit lo-fi, migas bauhaus varda oat milk retro activated charcoal tumblr synth polycule iPhone.</p>
<h2>Active Surveys</h2>
<p>Here's a list of currently active and completed surveys.</p>
<card>
<top>Sample Title</top>
<middle>
<p>Street art kettlebell burrata offal portra decolonize are.na akerman shabby chic blue bottle post-ironic breathwork. Roof party natural wine bell hooks cold plunge metrograph granny square ugh dad shoes garum before they sold out zines tahini side hustle. Indigo dye pork belly amaro, post-ironic health goth candle making van life hairpin robert walser.</p>
<bar><span>01/20/23</span> <span>Active</span></bar>
</middle>
<bottom>
<button-group>
<button type="button"><a href="/survey/">Take Survey</a></button>
</button-group>
</bottom>
</card>
<h2>Completed Surveys</h2>
<p>These surveys are no longer running, but you can see the results.</p>
<card>
<top>Sample Finished Survey</top>
<middle>
<p>Street art kettlebell burrata offal portra decolonize are.na akerman shabby chic blue bottle post-ironic breathwork. Roof party natural wine bell hooks cold plunge metrograph granny square ugh dad shoes garum before they sold out zines tahini side hustle. Indigo dye pork belly amaro, post-ironic health goth candle making van life hairpin robert walser.</p>
<bar><span>01/20/23</span> <span>Finished</span></bar>
</middle>
<bottom>
<button-group>
<button type="button"><a href="/survey/results">View Results</a></button>
</button-group>
</bottom>
</card>

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta name="viewport" content="initial-scale=1.0" />
<meta name="author" content="Zed A. Shaw" />
<meta name="description" content="My Go learning project, which is a Twitch support thing." />
<link rel="stylesheet" href="/style.css" />
<title>MY.com</title>
</head>
<body>
{{embed}}
</body>
</html>

@ -0,0 +1,35 @@
<script>
const login_form = (data, status) => {
if($no_error(data, status)) {
$redirect('/');
}
}
$boot(async () => {
$handle_form('login-form', login_form);
});
</script>
<div id='error'></div>
<template id='error-template'>
<aside id='error'><mark class="alert">${error}</mark></aside>
</template>
<div class="flex flex-col items-center p-6">
<form id='login-form' action="/api/login" method="POST">
<card>
<top><h2 style="color: white">Login</h2></top>
<middle>
<label for="username">Username</label>
<input id="username" name="username" placeholder="Username" type="text">
<label for="password">Password</label>
<input id="password" name="password" placeholder="Password" type="password">
</middle>
<bottom>
<button class="hover:btn-alert" type="button">Cancel</button>
<button class="hover:btn-hover" id="login-submit" type="submit">Login</button>
</bottom>
</card>
</form>
<div class="center"><a href="/register/">Need an account? Click to Register.</a></div>
</div>

@ -0,0 +1,20 @@
<div class="flex flex-col items-center p-6 min-w-md">
<form action="/api/register" method="POST">
<card>
<top><h2 style="color: white">Register</h2></top>
<middle>
<label for="username">Username</label>
<input id="username" name="username" placeholder="Username" type="text">
<label for="email">FAKE! Email</label>
<input id="email" name="email" placeholder="fake@faker.com" type="text">
<label for="password">Password</label>
<input id="password" name="password" placeholder="Password" type="password">
</middle>
<bottom>
<button class="hover:btn-alert" type="button">Cancel</button>
<button class="hover:btn-hover" id="register-submit" type="submit">Register</button>
</bottom>
</card>
</form>
<div class="center"><a href="/login/">Have an account? Click to Login.</a></div>
</div>

@ -0,0 +1,9 @@
<h1>Site Map</h1>
<ul>
{{range $index, $page := .Pages}}
<li><a href="{{$page}}">{{$page}}</a></li>
{{ else }}
Nothing Here
{{end}}
</ul>

@ -0,0 +1,462 @@
@theme default {
--font-sans:
ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
--font-mono:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
--color-red-50: oklch(97.1% 0.013 17.38);
--color-red-100: oklch(93.6% 0.032 17.717);
--color-red-200: oklch(88.5% 0.062 18.334);
--color-red-300: oklch(80.8% 0.114 19.571);
--color-red-400: oklch(70.4% 0.191 22.216);
--color-red-500: oklch(63.7% 0.237 25.331);
--color-red-600: oklch(57.7% 0.245 27.325);
--color-red-700: oklch(50.5% 0.213 27.518);
--color-red-800: oklch(44.4% 0.177 26.899);
--color-red-900: oklch(39.6% 0.141 25.723);
--color-red-950: oklch(25.8% 0.092 26.042);
--color-orange-50: oklch(98% 0.016 73.684);
--color-orange-100: oklch(95.4% 0.038 75.164);
--color-orange-200: oklch(90.1% 0.076 70.697);
--color-orange-300: oklch(83.7% 0.128 66.29);
--color-orange-400: oklch(75% 0.183 55.934);
--color-orange-500: oklch(70.5% 0.213 47.604);
--color-orange-600: oklch(64.6% 0.222 41.116);
--color-orange-700: oklch(55.3% 0.195 38.402);
--color-orange-800: oklch(47% 0.157 37.304);
--color-orange-900: oklch(40.8% 0.123 38.172);
--color-orange-950: oklch(26.6% 0.079 36.259);
--color-amber-50: oklch(98.7% 0.022 95.277);
--color-amber-100: oklch(96.2% 0.059 95.617);
--color-amber-200: oklch(92.4% 0.12 95.746);
--color-amber-300: oklch(87.9% 0.169 91.605);
--color-amber-400: oklch(82.8% 0.189 84.429);
--color-amber-500: oklch(76.9% 0.188 70.08);
--color-amber-600: oklch(66.6% 0.179 58.318);
--color-amber-700: oklch(55.5% 0.163 48.998);
--color-amber-800: oklch(47.3% 0.137 46.201);
--color-amber-900: oklch(41.4% 0.112 45.904);
--color-amber-950: oklch(27.9% 0.077 45.635);
--color-yellow-50: oklch(98.7% 0.026 102.212);
--color-yellow-100: oklch(97.3% 0.071 103.193);
--color-yellow-200: oklch(94.5% 0.129 101.54);
--color-yellow-300: oklch(90.5% 0.182 98.111);
--color-yellow-400: oklch(85.2% 0.199 91.936);
--color-yellow-500: oklch(79.5% 0.184 86.047);
--color-yellow-600: oklch(68.1% 0.162 75.834);
--color-yellow-700: oklch(55.4% 0.135 66.442);
--color-yellow-800: oklch(47.6% 0.114 61.907);
--color-yellow-900: oklch(42.1% 0.095 57.708);
--color-yellow-950: oklch(28.6% 0.066 53.813);
--color-lime-50: oklch(98.6% 0.031 120.757);
--color-lime-100: oklch(96.7% 0.067 122.328);
--color-lime-200: oklch(93.8% 0.127 124.321);
--color-lime-300: oklch(89.7% 0.196 126.665);
--color-lime-400: oklch(84.1% 0.238 128.85);
--color-lime-500: oklch(76.8% 0.233 130.85);
--color-lime-600: oklch(64.8% 0.2 131.684);
--color-lime-700: oklch(53.2% 0.157 131.589);
--color-lime-800: oklch(45.3% 0.124 130.933);
--color-lime-900: oklch(40.5% 0.101 131.063);
--color-lime-950: oklch(27.4% 0.072 132.109);
--color-green-50: oklch(98.2% 0.018 155.826);
--color-green-100: oklch(96.2% 0.044 156.743);
--color-green-200: oklch(92.5% 0.084 155.995);
--color-green-300: oklch(87.1% 0.15 154.449);
--color-green-400: oklch(79.2% 0.209 151.711);
--color-green-500: oklch(72.3% 0.219 149.579);
--color-green-600: oklch(62.7% 0.194 149.214);
--color-green-700: oklch(52.7% 0.154 150.069);
--color-green-800: oklch(44.8% 0.119 151.328);
--color-green-900: oklch(39.3% 0.095 152.535);
--color-green-950: oklch(26.6% 0.065 152.934);
--color-emerald-50: oklch(97.9% 0.021 166.113);
--color-emerald-100: oklch(95% 0.052 163.051);
--color-emerald-200: oklch(90.5% 0.093 164.15);
--color-emerald-300: oklch(84.5% 0.143 164.978);
--color-emerald-400: oklch(76.5% 0.177 163.223);
--color-emerald-500: oklch(69.6% 0.17 162.48);
--color-emerald-600: oklch(59.6% 0.145 163.225);
--color-emerald-700: oklch(50.8% 0.118 165.612);
--color-emerald-800: oklch(43.2% 0.095 166.913);
--color-emerald-900: oklch(37.8% 0.077 168.94);
--color-emerald-950: oklch(26.2% 0.051 172.552);
--color-teal-50: oklch(98.4% 0.014 180.72);
--color-teal-100: oklch(95.3% 0.051 180.801);
--color-teal-200: oklch(91% 0.096 180.426);
--color-teal-300: oklch(85.5% 0.138 181.071);
--color-teal-400: oklch(77.7% 0.152 181.912);
--color-teal-500: oklch(70.4% 0.14 182.503);
--color-teal-600: oklch(60% 0.118 184.704);
--color-teal-700: oklch(51.1% 0.096 186.391);
--color-teal-800: oklch(43.7% 0.078 188.216);
--color-teal-900: oklch(38.6% 0.063 188.416);
--color-teal-950: oklch(27.7% 0.046 192.524);
--color-cyan-50: oklch(98.4% 0.019 200.873);
--color-cyan-100: oklch(95.6% 0.045 203.388);
--color-cyan-200: oklch(91.7% 0.08 205.041);
--color-cyan-300: oklch(86.5% 0.127 207.078);
--color-cyan-400: oklch(78.9% 0.154 211.53);
--color-cyan-500: oklch(71.5% 0.143 215.221);
--color-cyan-600: oklch(60.9% 0.126 221.723);
--color-cyan-700: oklch(52% 0.105 223.128);
--color-cyan-800: oklch(45% 0.085 224.283);
--color-cyan-900: oklch(39.8% 0.07 227.392);
--color-cyan-950: oklch(30.2% 0.056 229.695);
--color-sky-50: oklch(97.7% 0.013 236.62);
--color-sky-100: oklch(95.1% 0.026 236.824);
--color-sky-200: oklch(90.1% 0.058 230.902);
--color-sky-300: oklch(82.8% 0.111 230.318);
--color-sky-400: oklch(74.6% 0.16 232.661);
--color-sky-500: oklch(68.5% 0.169 237.323);
--color-sky-600: oklch(58.8% 0.158 241.966);
--color-sky-700: oklch(50% 0.134 242.749);
--color-sky-800: oklch(44.3% 0.11 240.79);
--color-sky-900: oklch(39.1% 0.09 240.876);
--color-sky-950: oklch(29.3% 0.066 243.157);
--color-blue-50: oklch(97% 0.014 254.604);
--color-blue-100: oklch(93.2% 0.032 255.585);
--color-blue-200: oklch(88.2% 0.059 254.128);
--color-blue-300: oklch(80.9% 0.105 251.813);
--color-blue-400: oklch(70.7% 0.165 254.624);
--color-blue-500: oklch(62.3% 0.214 259.815);
--color-blue-600: oklch(54.6% 0.245 262.881);
--color-blue-700: oklch(48.8% 0.243 264.376);
--color-blue-800: oklch(42.4% 0.199 265.638);
--color-blue-900: oklch(37.9% 0.146 265.522);
--color-blue-950: oklch(28.2% 0.091 267.935);
--color-indigo-50: oklch(96.2% 0.018 272.314);
--color-indigo-100: oklch(93% 0.034 272.788);
--color-indigo-200: oklch(87% 0.065 274.039);
--color-indigo-300: oklch(78.5% 0.115 274.713);
--color-indigo-400: oklch(67.3% 0.182 276.935);
--color-indigo-500: oklch(58.5% 0.233 277.117);
--color-indigo-600: oklch(51.1% 0.262 276.966);
--color-indigo-700: oklch(45.7% 0.24 277.023);
--color-indigo-800: oklch(39.8% 0.195 277.366);
--color-indigo-900: oklch(35.9% 0.144 278.697);
--color-indigo-950: oklch(25.7% 0.09 281.288);
--color-violet-50: oklch(96.9% 0.016 293.756);
--color-violet-100: oklch(94.3% 0.029 294.588);
--color-violet-200: oklch(89.4% 0.057 293.283);
--color-violet-300: oklch(81.1% 0.111 293.571);
--color-violet-400: oklch(70.2% 0.183 293.541);
--color-violet-500: oklch(60.6% 0.25 292.717);
--color-violet-600: oklch(54.1% 0.281 293.009);
--color-violet-700: oklch(49.1% 0.27 292.581);
--color-violet-800: oklch(43.2% 0.232 292.759);
--color-violet-900: oklch(38% 0.189 293.745);
--color-violet-950: oklch(28.3% 0.141 291.089);
--color-purple-50: oklch(97.7% 0.014 308.299);
--color-purple-100: oklch(94.6% 0.033 307.174);
--color-purple-200: oklch(90.2% 0.063 306.703);
--color-purple-300: oklch(82.7% 0.119 306.383);
--color-purple-400: oklch(71.4% 0.203 305.504);
--color-purple-500: oklch(62.7% 0.265 303.9);
--color-purple-600: oklch(55.8% 0.288 302.321);
--color-purple-700: oklch(49.6% 0.265 301.924);
--color-purple-800: oklch(43.8% 0.218 303.724);
--color-purple-900: oklch(38.1% 0.176 304.987);
--color-purple-950: oklch(29.1% 0.149 302.717);
--color-fuchsia-50: oklch(97.7% 0.017 320.058);
--color-fuchsia-100: oklch(95.2% 0.037 318.852);
--color-fuchsia-200: oklch(90.3% 0.076 319.62);
--color-fuchsia-300: oklch(83.3% 0.145 321.434);
--color-fuchsia-400: oklch(74% 0.238 322.16);
--color-fuchsia-500: oklch(66.7% 0.295 322.15);
--color-fuchsia-600: oklch(59.1% 0.293 322.896);
--color-fuchsia-700: oklch(51.8% 0.253 323.949);
--color-fuchsia-800: oklch(45.2% 0.211 324.591);
--color-fuchsia-900: oklch(40.1% 0.17 325.612);
--color-fuchsia-950: oklch(29.3% 0.136 325.661);
--color-pink-50: oklch(97.1% 0.014 343.198);
--color-pink-100: oklch(94.8% 0.028 342.258);
--color-pink-200: oklch(89.9% 0.061 343.231);
--color-pink-300: oklch(82.3% 0.12 346.018);
--color-pink-400: oklch(71.8% 0.202 349.761);
--color-pink-500: oklch(65.6% 0.241 354.308);
--color-pink-600: oklch(59.2% 0.249 0.584);
--color-pink-700: oklch(52.5% 0.223 3.958);
--color-pink-800: oklch(45.9% 0.187 3.815);
--color-pink-900: oklch(40.8% 0.153 2.432);
--color-pink-950: oklch(28.4% 0.109 3.907);
--color-rose-50: oklch(96.9% 0.015 12.422);
--color-rose-100: oklch(94.1% 0.03 12.58);
--color-rose-200: oklch(89.2% 0.058 10.001);
--color-rose-300: oklch(81% 0.117 11.638);
--color-rose-400: oklch(71.2% 0.194 13.428);
--color-rose-500: oklch(64.5% 0.246 16.439);
--color-rose-600: oklch(58.6% 0.253 17.585);
--color-rose-700: oklch(51.4% 0.222 16.935);
--color-rose-800: oklch(45.5% 0.188 13.697);
--color-rose-900: oklch(41% 0.159 10.272);
--color-rose-950: oklch(27.1% 0.105 12.094);
--color-slate-50: oklch(98.4% 0.003 247.858);
--color-slate-100: oklch(96.8% 0.007 247.896);
--color-slate-200: oklch(92.9% 0.013 255.508);
--color-slate-300: oklch(86.9% 0.022 252.894);
--color-slate-400: oklch(70.4% 0.04 256.788);
--color-slate-500: oklch(55.4% 0.046 257.417);
--color-slate-600: oklch(44.6% 0.043 257.281);
--color-slate-700: oklch(37.2% 0.044 257.287);
--color-slate-800: oklch(27.9% 0.041 260.031);
--color-slate-900: oklch(20.8% 0.042 265.755);
--color-slate-950: oklch(12.9% 0.042 264.695);
--color-gray-50: oklch(98.5% 0.002 247.839);
--color-gray-100: oklch(96.7% 0.003 264.542);
--color-gray-200: oklch(92.8% 0.006 264.531);
--color-gray-300: oklch(87.2% 0.01 258.338);
--color-gray-400: oklch(70.7% 0.022 261.325);
--color-gray-500: oklch(55.1% 0.027 264.364);
--color-gray-600: oklch(44.6% 0.03 256.802);
--color-gray-700: oklch(37.3% 0.034 259.733);
--color-gray-800: oklch(27.8% 0.033 256.848);
--color-gray-900: oklch(21% 0.034 264.665);
--color-gray-950: oklch(13% 0.028 261.692);
--color-zinc-50: oklch(98.5% 0 0);
--color-zinc-100: oklch(96.7% 0.001 286.375);
--color-zinc-200: oklch(92% 0.004 286.32);
--color-zinc-300: oklch(87.1% 0.006 286.286);
--color-zinc-400: oklch(70.5% 0.015 286.067);
--color-zinc-500: oklch(55.2% 0.016 285.938);
--color-zinc-600: oklch(44.2% 0.017 285.786);
--color-zinc-700: oklch(37% 0.013 285.805);
--color-zinc-800: oklch(27.4% 0.006 286.033);
--color-zinc-900: oklch(21% 0.006 285.885);
--color-zinc-950: oklch(14.1% 0.005 285.823);
--color-neutral-50: oklch(98.5% 0 0);
--color-neutral-100: oklch(97% 0 0);
--color-neutral-200: oklch(92.2% 0 0);
--color-neutral-300: oklch(87% 0 0);
--color-neutral-400: oklch(70.8% 0 0);
--color-neutral-500: oklch(55.6% 0 0);
--color-neutral-600: oklch(43.9% 0 0);
--color-neutral-700: oklch(37.1% 0 0);
--color-neutral-800: oklch(26.9% 0 0);
--color-neutral-900: oklch(20.5% 0 0);
--color-neutral-950: oklch(14.5% 0 0);
--color-stone-50: oklch(98.5% 0.001 106.423);
--color-stone-100: oklch(97% 0.001 106.424);
--color-stone-200: oklch(92.3% 0.003 48.717);
--color-stone-300: oklch(86.9% 0.005 56.366);
--color-stone-400: oklch(70.9% 0.01 56.259);
--color-stone-500: oklch(55.3% 0.013 58.071);
--color-stone-600: oklch(44.4% 0.011 73.639);
--color-stone-700: oklch(37.4% 0.01 67.558);
--color-stone-800: oklch(26.8% 0.007 34.298);
--color-stone-900: oklch(21.6% 0.006 56.043);
--color-stone-950: oklch(14.7% 0.004 49.25);
--color-black: #000;
--color-white: #fff;
--spacing: 0.25rem;
--breakpoint-sm: 40rem;
--breakpoint-md: 48rem;
--breakpoint-lg: 64rem;
--breakpoint-xl: 80rem;
--breakpoint-2xl: 96rem;
--container-3xs: 16rem;
--container-2xs: 18rem;
--container-xs: 20rem;
--container-sm: 24rem;
--container-md: 28rem;
--container-lg: 32rem;
--container-xl: 36rem;
--container-2xl: 42rem;
--container-3xl: 48rem;
--container-4xl: 56rem;
--container-5xl: 64rem;
--container-6xl: 72rem;
--container-7xl: 80rem;
--text-xs: 0.75rem;
--text-xs--line-height: calc(1 / 0.75);
--text-sm: 0.875rem;
--text-sm--line-height: calc(1.25 / 0.875);
--text-base: 1rem;
--text-base--line-height: calc(1.5 / 1);
--text-lg: 1.125rem;
--text-lg--line-height: calc(1.75 / 1.125);
--text-xl: 1.25rem;
--text-xl--line-height: calc(1.75 / 1.25);
--text-2xl: 1.5rem;
--text-2xl--line-height: calc(2 / 1.5);
--text-3xl: 1.875rem;
--text-3xl--line-height: calc(2.25 / 1.875);
--text-4xl: 2.25rem;
--text-4xl--line-height: calc(2.5 / 2.25);
--text-5xl: 3rem;
--text-5xl--line-height: 1;
--text-6xl: 3.75rem;
--text-6xl--line-height: 1;
--text-7xl: 4.5rem;
--text-7xl--line-height: 1;
--text-8xl: 6rem;
--text-8xl--line-height: 1;
--text-9xl: 8rem;
--text-9xl--line-height: 1;
--font-weight-thin: 100;
--font-weight-extralight: 200;
--font-weight-light: 300;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-weight-extrabold: 800;
--font-weight-black: 900;
--tracking-tighter: -0.05em;
--tracking-tight: -0.025em;
--tracking-normal: 0em;
--tracking-wide: 0.025em;
--tracking-wider: 0.05em;
--tracking-widest: 0.1em;
--leading-tight: 1.25;
--leading-snug: 1.375;
--leading-normal: 1.5;
--leading-relaxed: 1.625;
--leading-loose: 2;
--radius-xs: 0.125rem;
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-2xl: 1rem;
--radius-3xl: 1.5rem;
--radius-4xl: 2rem;
--shadow-2xs: 0 1px rgb(0 0 0 / 0.05);
--shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
--shadow-2xl: 0 25px 50px -12px rgb(0 0 0 / 0.25);
--inset-shadow-2xs: inset 0 1px rgb(0 0 0 / 0.05);
--inset-shadow-xs: inset 0 1px 1px rgb(0 0 0 / 0.05);
--inset-shadow-sm: inset 0 2px 4px rgb(0 0 0 / 0.05);
--drop-shadow-xs: 0 1px 1px rgb(0 0 0 / 0.05);
--drop-shadow-sm: 0 1px 2px rgb(0 0 0 / 0.15);
--drop-shadow-md: 0 3px 3px rgb(0 0 0 / 0.12);
--drop-shadow-lg: 0 4px 4px rgb(0 0 0 / 0.15);
--drop-shadow-xl: 0 9px 7px rgb(0 0 0 / 0.1);
--drop-shadow-2xl: 0 25px 25px rgb(0 0 0 / 0.15);
--text-shadow-2xs: 0px 1px 0px rgb(0 0 0 / 0.15);
--text-shadow-xs: 0px 1px 1px rgb(0 0 0 / 0.2);
--text-shadow-sm:
0px 1px 0px rgb(0 0 0 / 0.075), 0px 1px 1px rgb(0 0 0 / 0.075), 0px 2px 2px rgb(0 0 0 / 0.075);
--text-shadow-md:
0px 1px 1px rgb(0 0 0 / 0.1), 0px 1px 2px rgb(0 0 0 / 0.1), 0px 2px 4px rgb(0 0 0 / 0.1);
--text-shadow-lg:
0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), 0px 4px 8px rgb(0 0 0 / 0.1);
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--animate-spin: spin 1s linear infinite;
--animate-ping: ping 1s cubic-bezier(0, 0, 0.2, 1) infinite;
--animate-pulse: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
--animate-bounce: bounce 1s infinite;
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@keyframes ping {
75%,
100% {
transform: scale(2);
opacity: 0;
}
}
@keyframes pulse {
50% {
opacity: 0.5;
}
}
@keyframes bounce {
0%,
100% {
transform: translateY(-25%);
animation-timing-function: cubic-bezier(0.8, 0, 1, 1);
}
50% {
transform: none;
animation-timing-function: cubic-bezier(0, 0, 0.2, 1);
}
}
--blur-xs: 4px;
--blur-sm: 8px;
--blur-md: 12px;
--blur-lg: 16px;
--blur-xl: 24px;
--blur-2xl: 40px;
--blur-3xl: 64px;
--perspective-dramatic: 100px;
--perspective-near: 300px;
--perspective-normal: 500px;
--perspective-midrange: 800px;
--perspective-distant: 1200px;
--aspect-video: 16 / 9;
--default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
--default-font-family: --theme(--font-sans, initial);
--default-font-feature-settings: --theme(--font-sans--font-feature-settings, initial);
--default-font-variation-settings: --theme(--font-sans--font-variation-settings, initial);
--default-mono-font-family: --theme(--font-mono, initial);
--default-mono-font-feature-settings: --theme(--font-mono--font-feature-settings, initial);
--default-mono-font-variation-settings: --theme(--font-mono--font-variation-settings, initial);
}
/* Deprecated */
@theme default inline reference {
--blur: 8px;
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--shadow-inner: inset 0 2px 4px 0 rgb(0 0 0 / 0.05);
--drop-shadow: 0 1px 2px rgb(0 0 0 / 0.1), 0 1px 1px rgb(0 0 0 / 0.06);
--radius: 0.25rem;
--max-width-prose: 65ch;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

@ -0,0 +1,15 @@
ISC License
Icons are Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

@ -0,0 +1,17 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="16" cy="4" r="1" />
<path d="m18 19 1-7-5.87.94" />
<path d="m5 8 3-3 5.5 3-2.21 3.1" />
<path d="M4.24 14.48c-.19.58-.27 1.2-.23 1.84a5 5 0 0 0 5.31 4.67c.65-.04 1.25-.2 1.8-.46" />
<path d="M13.76 17.52c.19-.58.27-1.2.23-1.84a5 5 0 0 0-5.31-4.67c-.65.04-1.25.2-1.8.46" />
</svg>

After

Width:  |  Height:  |  Size: 504 B

@ -0,0 +1,13 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
</svg>

After

Width:  |  Height:  |  Size: 264 B

@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M6 12H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2" />
<path d="M6 8h12" />
<path d="M18.3 17.7a2.5 2.5 0 0 1-3.16 3.83 2.53 2.53 0 0 1-1.14-2V12" />
<path d="M6.6 15.6A2 2 0 1 0 10 17v-5" />
</svg>

After

Width:  |  Height:  |  Size: 440 B

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 17H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-1" />
<polygon points="12 15 17 21 7 21 12 15" />
</svg>

After

Width:  |  Height:  |  Size: 344 B

@ -0,0 +1,18 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 21a8 8 0 1 0 0-16 8 8 0 0 0 0 16z" />
<path d="M5 3 2 6" />
<path d="m22 6-3-3" />
<path d="m6 19-2 2" />
<path d="m18 19 2 2" />
<path d="m9 13 2 2 4-4" />
</svg>

After

Width:  |  Height:  |  Size: 390 B

@ -0,0 +1,18 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M6.87 6.87a8 8 0 1 0 11.26 11.26" />
<path d="M19.9 14.25A7.44 7.44 0 0 0 20 13a8 8 0 0 0-8-8 7.44 7.44 0 0 0-1.25.1" />
<path d="m22 6-3-3" />
<path d="m6 19-2 2" />
<path d="m2 2 20 20" />
<path d="M4 4 2 6" />
</svg>

After

Width:  |  Height:  |  Size: 442 B

@ -0,0 +1,18 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="13" r="8" />
<path d="M12 9v4l2 2" />
<path d="M5 3 2 6" />
<path d="m22 6-3-3" />
<path d="m6 19-2 2" />
<path d="m18 19 2 2" />
</svg>

After

Width:  |  Height:  |  Size: 370 B

@ -0,0 +1,18 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 21a8 8 0 1 0 0-16 8 8 0 0 0 0 16z" />
<path d="M5 3 2 6" />
<path d="m22 6-3-3" />
<path d="m6 19-2 2" />
<path d="m18 19 2 2" />
<path d="M9 13h6" />
</svg>

After

Width:  |  Height:  |  Size: 384 B

@ -0,0 +1,19 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 21a8 8 0 1 0 0-16 8 8 0 0 0 0 16z" />
<path d="M5 3 2 6" />
<path d="m22 6-3-3" />
<path d="m6 19-2 2" />
<path d="m18 19 2 2" />
<path d="M12 10v6" />
<path d="M9 13h6" />
</svg>

After

Width:  |  Height:  |  Size: 408 B

@ -0,0 +1,14 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<polyline points="11 3 11 11 14 8 17 11 17 3" />
</svg>

After

Width:  |  Height:  |  Size: 319 B

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>

After

Width:  |  Height:  |  Size: 332 B

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2" />
<line x1="12" y1="8" x2="12" y2="12" />
<line x1="12" y1="16" x2="12.01" y2="16" />
</svg>

After

Width:  |  Height:  |  Size: 390 B

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>

After

Width:  |  Height:  |  Size: 385 B

@ -0,0 +1,17 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M2 12h20" />
<path d="M10 16v4a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-4" />
<path d="M10 8V4a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v4" />
<path d="M20 16v1a2 2 0 0 1-2 2h-2a2 2 0 0 1-2-2v-1" />
<path d="M14 8V7c0-1.1.9-2 2-2h2a2 2 0 0 1 2 2v1" />
</svg>

After

Width:  |  Height:  |  Size: 457 B

@ -0,0 +1,17 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 2v20" />
<path d="M8 10H4a2 2 0 0 1-2-2V6c0-1.1.9-2 2-2h4" />
<path d="M16 10h4a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-4" />
<path d="M8 20H7a2 2 0 0 1-2-2v-2c0-1.1.9-2 2-2h1" />
<path d="M16 14h1a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2h-1" />
</svg>

After

Width:  |  Height:  |  Size: 457 B

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="21" y1="6" x2="3" y2="6" />
<line x1="17" y1="12" x2="7" y2="12" />
<line x1="19" y1="18" x2="5" y2="18" />
</svg>

After

Width:  |  Height:  |  Size: 332 B

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="4" y="2" width="6" height="16" rx="2" />
<rect x="14" y="9" width="6" height="9" rx="2" />
<path d="M22 22H2" />
</svg>

After

Width:  |  Height:  |  Size: 336 B

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="2" y="4" width="16" height="6" rx="2" />
<rect x="9" y="14" width="9" height="6" rx="2" />
<path d="M22 22V2" />
</svg>

After

Width:  |  Height:  |  Size: 336 B

@ -0,0 +1,18 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="4" y="5" width="6" height="14" rx="2" />
<rect x="14" y="7" width="6" height="10" rx="2" />
<path d="M17 22v-5" />
<path d="M17 7V2" />
<path d="M7 22v-3" />
<path d="M7 5V2" />
</svg>

After

Width:  |  Height:  |  Size: 407 B

@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="4" y="5" width="6" height="14" rx="2" />
<rect x="14" y="7" width="6" height="10" rx="2" />
<path d="M10 2v20" />
<path d="M20 2v20" />
</svg>

After

Width:  |  Height:  |  Size: 361 B

@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="4" y="5" width="6" height="14" rx="2" />
<rect x="14" y="7" width="6" height="10" rx="2" />
<path d="M4 2v20" />
<path d="M14 2v20" />
</svg>

After

Width:  |  Height:  |  Size: 360 B

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="2" y="5" width="6" height="14" rx="2" />
<rect x="16" y="7" width="6" height="10" rx="2" />
<path d="M12 2v20" />
</svg>

After

Width:  |  Height:  |  Size: 337 B

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="2" y="5" width="6" height="14" rx="2" />
<rect x="12" y="7" width="6" height="10" rx="2" />
<path d="M22 2v20" />
</svg>

After

Width:  |  Height:  |  Size: 337 B

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="6" y="5" width="6" height="14" rx="2" />
<rect x="16" y="7" width="6" height="10" rx="2" />
<path d="M2 2v20" />
</svg>

After

Width:  |  Height:  |  Size: 336 B

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="9" y="7" width="6" height="10" rx="2" />
<path d="M4 22V2" />
<path d="M20 22V2" />
</svg>

After

Width:  |  Height:  |  Size: 307 B

@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="5" width="6" height="14" rx="2" />
<rect x="15" y="7" width="6" height="10" rx="2" />
<path d="M3 2v20" />
<path d="M21 2v20" />
</svg>

After

Width:  |  Height:  |  Size: 360 B

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>

After

Width:  |  Height:  |  Size: 332 B

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="21" y1="6" x2="3" y2="6" />
<line x1="15" y1="12" x2="3" y2="12" />
<line x1="17" y1="18" x2="3" y2="18" />
</svg>

After

Width:  |  Height:  |  Size: 332 B

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="21" y1="6" x2="3" y2="6" />
<line x1="21" y1="12" x2="9" y2="12" />
<line x1="21" y1="18" x2="7" y2="18" />
</svg>

After

Width:  |  Height:  |  Size: 332 B

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="4" y="6" width="6" height="16" rx="2" />
<rect x="14" y="6" width="6" height="9" rx="2" />
<path d="M22 2H2" />
</svg>

After

Width:  |  Height:  |  Size: 335 B

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="6" y="14" width="9" height="6" rx="2" />
<rect x="6" y="4" width="16" height="6" rx="2" />
<path d="M2 2v20" />
</svg>

After

Width:  |  Height:  |  Size: 335 B

@ -0,0 +1,18 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="5" y="14" width="14" height="6" rx="2" />
<rect x="7" y="4" width="10" height="6" rx="2" />
<path d="M22 7h-5" />
<path d="M7 7H1" />
<path d="M22 17h-3" />
<path d="M5 17H2" />
</svg>

After

Width:  |  Height:  |  Size: 407 B

@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="5" y="14" width="14" height="6" rx="2" />
<rect x="7" y="4" width="10" height="6" rx="2" />
<path d="M2 20h20" />
<path d="M2 10h20" />
</svg>

After

Width:  |  Height:  |  Size: 361 B

@ -0,0 +1,16 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="5" y="14" width="14" height="6" rx="2" />
<rect x="7" y="4" width="10" height="6" rx="2" />
<path d="M2 14h20" />
<path d="M2 4h20" />
</svg>

After

Width:  |  Height:  |  Size: 360 B

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="5" y="16" width="14" height="6" rx="2" />
<rect x="7" y="2" width="10" height="6" rx="2" />
<path d="M2 12h20" />
</svg>

After

Width:  |  Height:  |  Size: 337 B

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="5" y="12" width="14" height="6" rx="2" />
<rect x="7" y="2" width="10" height="6" rx="2" />
<path d="M2 22h20" />
</svg>

After

Width:  |  Height:  |  Size: 337 B

@ -0,0 +1,15 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="5" y="16" width="14" height="6" rx="2" />
<rect x="7" y="6" width="10" height="6" rx="2" />
<path d="M2 2h20" />
</svg>

After

Width:  |  Height:  |  Size: 336 B

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save