├── .gitignore
├── dotenv
├── go.mod
├── tailwind.config.js
├── input.css
├── Makefile
├── middleware.go
├── main.go
├── handlers_test.go
├── db_test.go
├── middleware_test.go
├── db.go
├── global_test.go
├── README.md
├── components.templ
├── handlers.go
├── global.go
├── LICENCE
└── static
├── css
└── styles.css
└── js
└── htmx.min.js
/.gitignore:
--------------------------------------------------------------------------------
1 | *.env
2 | bin/
3 | *.sum
4 | *_templ.go
5 |
--------------------------------------------------------------------------------
/dotenv:
--------------------------------------------------------------------------------
1 | DB_USER="Your mariadb user here"
2 | DB_PASS="Your mariadb password here"
3 | DB_NAME="Your database name here"
4 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/morethancoder/new_gotham
2 |
3 | go 1.21.4
4 |
5 | require (
6 | github.com/a-h/templ v0.2.476
7 | github.com/go-chi/chi/v5 v5.0.11
8 | github.com/go-sql-driver/mysql v1.7.1
9 | github.com/joho/godotenv v1.5.1
10 | )
11 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | './**/*.templ',
5 | ],
6 | theme: {
7 | extend: {
8 | colors: {
9 | },
10 | },
11 | },
12 | plugins: [],
13 | };
14 |
15 |
--------------------------------------------------------------------------------
/input.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 |
6 |
7 | .button {
8 | @apply cursor-pointer hover:bg-blue-300 active:bg-green-500
9 | text-center text-slate-500 font-bold uppercase bg-blue-500
10 | select-none p-2 rounded-lg
11 | }
12 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | build:
2 | go build -o bin/app
3 |
4 | run: tailwindcss templ build
5 | ./bin/app
6 |
7 | test:
8 | go test -v ./... -count=1
9 |
10 |
11 | tailwindcss:
12 | bun run tailwindcss --config tailwind.config.js -i input.css -o static/css/styles.css
13 |
14 | templ:
15 | templ generate
16 |
17 |
--------------------------------------------------------------------------------
/middleware.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "database/sql"
6 | "net/http"
7 | )
8 |
9 | type key int
10 |
11 | const (
12 | DbClientKey key = iota
13 | )
14 |
15 | func DbClientMiddleware(dbClient *sql.DB) func(http.Handler) http.Handler {
16 | return func(h http.Handler) http.Handler {
17 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18 | ctx := context.WithValue(r.Context(), DbClientKey, dbClient)
19 | h.ServeHTTP(w, r.WithContext(ctx))
20 | })
21 | }
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | "github.com/go-chi/chi/v5"
7 | )
8 |
9 |
10 | func main() {
11 | dbClient, err := InitDatabase(".env")
12 | if err != nil {log.Fatal(err)}
13 | err = InitGlobalValuesTable(dbClient)
14 | if err != nil {log.Fatal(err)}
15 | r := chi.NewRouter()
16 | r.Use(DbClientMiddleware(dbClient))
17 | fs := http.FileServer(http.Dir("static"))
18 | r.Handle("/static/*", http.StripPrefix("/static/", fs))
19 | r.Get("/", HomeGetHandler)
20 | r.Post("/count", CountPostHandler)
21 | log.Println("running on >> http://127.0.0.1:8000")
22 | err = http.ListenAndServe(":8000", r)
23 | if err != nil {
24 | log.Fatal(err)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/handlers_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "testing"
7 | )
8 |
9 | func TestRenderViewHandlers(t *testing.T) {
10 | routePath := "/"
11 | view := Home("Hello", "message", 10)
12 | reqHx, err := http.NewRequest("GET", routePath, nil)
13 | if err != nil {t.Fatal(err)}
14 | reqHx.Header.Set("Hx-Request", "true")
15 |
16 | req, err := http.NewRequest("GET", routePath, nil)
17 | if err != nil {t.Fatal(err)}
18 |
19 | rrHx := httptest.NewRecorder()
20 | rr := httptest.NewRecorder()
21 |
22 | RenderView(rrHx, reqHx, view, routePath)
23 | if status := rrHx.Code; status != http.StatusOK {
24 | t.Errorf("Render with hx returned wrong status: got %v want %v", status, http.StatusOK)
25 | }
26 |
27 | RenderView(rr, req, view, routePath)
28 | if status := rr.Code; status != http.StatusOK {
29 | t.Errorf("Render without hx returned wrong status: got %v want %v", status, http.StatusOK)
30 | }
31 | }
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/db_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "testing"
7 |
8 | "github.com/joho/godotenv"
9 | )
10 |
11 | const DotenvPath = ".env"
12 |
13 | func TestDatabaseInit(t *testing.T) {
14 | err := godotenv.Load(DotenvPath); if err != nil { t.Fatalf(err.Error()) }
15 | dbName := os.Getenv("DB_NAME")
16 |
17 | dbClient, err := InitDatabase(DotenvPath)
18 | if err != nil { t.Fatalf("Unable to create dbclient: %v", err) }
19 | defer dbClient.Close()
20 | rows, err := dbClient.Query("SHOW DATABASES LIKE " + fmt.Sprintf(`"%s"`,dbName))
21 | if err != nil { t.Fatalf("Database check exsistance failed: %v", err) }
22 | defer rows.Close()
23 |
24 | if !rows.Next() {
25 | t.Fatalf("Database doesnt exist after creation: %v", err)
26 | } else {
27 | _, err := dbClient.Exec("DROP DATABASE IF EXISTS " + dbName)
28 | if err != nil { t.Fatalf("Unable to delete database: %v", err) }
29 | t.Logf("Database (%s) Created and Deleted seccussfully!", dbName)
30 | }
31 | }
32 |
33 |
--------------------------------------------------------------------------------
/middleware_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "database/sql"
5 | "net/http"
6 | "net/http/httptest"
7 | "testing"
8 | "github.com/go-chi/chi/v5"
9 | )
10 |
11 | func TestDatabaseMiddleware(t *testing.T) {
12 | dbClient := &sql.DB{}
13 | testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
14 | dbClient, ok := r.Context().Value(DbClientKey).(*sql.DB)
15 | if !ok {
16 | t.Fatal("Couldnt find dbClient from Context!")
17 | }
18 | _ = dbClient
19 |
20 | w.WriteHeader(http.StatusOK)
21 | })
22 |
23 | r := chi.NewRouter()
24 | r.Use(DbClientMiddleware(dbClient))
25 | r.Get("/test", testHandler)
26 |
27 | req, err := http.NewRequest("GET","/test", nil)
28 | if err != nil {t.Fatal(err)}
29 |
30 | recorder := httptest.NewRecorder()
31 | r.ServeHTTP(recorder, req)
32 |
33 | if status := recorder.Code; status != http.StatusOK {
34 | t.Errorf("Test Handler returned wrong status code: got:%d want:%d", recorder.Code, http.StatusOK )
35 | }
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/db.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "database/sql"
5 | "fmt"
6 | "os"
7 |
8 | _ "github.com/go-sql-driver/mysql"
9 | "github.com/joho/godotenv"
10 | )
11 |
12 | func InitDatabase(dotenvPath string) (*sql.DB, error) {
13 | err := godotenv.Load(dotenvPath)
14 | if err != nil {return nil, err}
15 |
16 | dbUser := os.Getenv("DB_USER")
17 | dbPass := os.Getenv("DB_PASS")
18 | dbName := os.Getenv("DB_NAME")
19 | dbURL := fmt.Sprintf("%s:%s@/", dbUser, dbPass)
20 |
21 | db, err := sql.Open("mysql", dbURL)
22 | if err != nil {return nil, err}
23 |
24 | // Check if the database exists
25 | rows, err := db.Query("SHOW DATABASES LIKE " + fmt.Sprintf(`"%s"`, dbName))
26 | if err != nil {return nil, err}
27 | defer rows.Close()
28 |
29 | if !rows.Next() {
30 | // Create the database if it doesn't exist
31 | _, err := db.Exec("CREATE DATABASE " + dbName)
32 | if err != nil {return nil, err}
33 | }
34 |
35 | // Close the current connection and reconnect to the specific database
36 | db.Close()
37 |
38 | dbURLWithDbName:= fmt.Sprintf("%s:%s@/%s", dbUser, dbPass, dbName)
39 | db, err = sql.Open("mysql", dbURLWithDbName)
40 | if err != nil {return nil, err}
41 |
42 | return db, nil
43 | }
44 |
45 |
--------------------------------------------------------------------------------
/global_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os"
5 | "testing"
6 |
7 | "github.com/joho/godotenv"
8 | )
9 |
10 |
11 | func TestGlobalValues(t *testing.T) {
12 | err := godotenv.Load(DotenvPath); if err != nil { t.Fatalf(err.Error()) }
13 | dbName := os.Getenv("DB_NAME")
14 |
15 | dbClient, err := InitDatabase(DotenvPath); if err != nil {t.Fatalf(err.Error())}
16 | err = InitGlobalValuesTable(dbClient); if err != nil {t.Fatalf(err.Error())}
17 | i := GlobalValuesInstance{
18 | Count: 0,
19 | }
20 | err = i.Create(dbClient); if err != nil {t.Fatalf(err.Error())}
21 | var foo GlobalValuesInstance = GlobalValuesInstance{ID: 1}
22 |
23 | err = foo.Create(dbClient); if err != nil {t.Fatal(err.Error())}
24 | err = foo.Read(dbClient); if err != nil {t.Fatalf(err.Error())}
25 | t.Logf("GlobalCount: ID:%d Count:%d", i.ID, i.Count)
26 | err = foo.Update(dbClient); if err != nil {t.Fatalf(err.Error())}
27 | t.Logf("GlobalCount: ID:%d Count:%d", foo.ID, foo.Count)
28 | if foo.Count == 0 {
29 | t.Fatalf("Incremented count shouldnt equal to zero")
30 | }
31 | err = foo.Delete(dbClient); if err != nil {t.Fatalf(err.Error())}
32 | _, err = dbClient.Exec("DROP TABLE "+ GlobalValuesTableKey); if err != nil {t.Fatalf(err.Error())}
33 | _, err = dbClient.Exec("DROP DATABASE IF EXISTS "+ dbName); if err != nil {t.Fatalf(err.Error())}
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## GOTHAM Stack Starter App
2 |
3 | welcome to the most basic app to get you started with the amazing SSR webstack (GOTHAM).
4 | more in depth examples (session, routing, todo app) found [here](https://github.com/morethancoder/hello_gotham).
5 |
6 | ### Prerequisites
7 |
8 | Ensure the following dependencies are installed before setting up and running the app:
9 |
10 | * If you are on **MacOS** and have [homebrew](https://brew.sh) installed, simply run:
11 |
12 | ```bash
13 | # macOS users with Homebrew
14 | brew install go git mariadb node
15 | ```
16 |
17 |
18 | * For linux or windows users kindly follow official instructions:
19 |
20 | - [Golang](https://golang.org/dl/)
21 | - [Git](https://git-scm.com/)
22 | - [MariaDB](https://mariadb.org/download/)
23 | - [Node.js](https://nodejs.org/)
24 |
25 |
26 | ### Tailwindcss and Templ
27 |
28 | ```bash
29 | # Bun
30 | curl -fsSL https://bun.sh/install | bash
31 |
32 | # Tailwind CSS using Bun
33 | bun install -g tailwindcss@latest
34 |
35 | # Templ
36 | go install github.com/a-h/templ/cmd/templ@latest
37 | ```
38 |
39 | ### Usage
40 | * clone this repo:
41 | ```bash
42 | git clone https://github.com/morethancoder/new_gotham
43 | ```
44 | * make sure your mariadb server is running.
45 | * create a .env file inside the project directory, and fill database credentials:
46 | ```bash
47 | cd new_project && mv dotenv .env
48 | ```
49 | * make it yours, edit go.mod module name (optional):
50 | ```bash
51 | go mod edit -module="your project name"
52 | ```
53 | * tidy project go dependencies:
54 | ```bash
55 | go mod tidy
56 | ```
57 | * templ:
58 | ```bash
59 | make templ
60 | ```
61 | * test:
62 | ```bash
63 | make test
64 | ```
65 | * run:
66 | ```bash
67 | make run
68 | ```
69 |
70 | * enjoy the best SSR web stack
71 |
72 |
73 |
--------------------------------------------------------------------------------
/components.templ:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "strconv"
4 |
5 | script TypingEffect(word, id string, delay int) {
6 | let index = 0;
7 | const displayArea = document.getElementById(id);
8 |
9 | const intervalId = setInterval(function () {
10 | if (index < word.length) {
11 | displayArea.textContent += word[index];
12 | index++;
13 | } else {
14 | clearInterval(intervalId);
15 | }
16 | }, delay);
17 | }
18 |
19 | templ Layout(route string) {
20 |
21 |
22 |
23 |
24 |
25 |
26 | GOTHAM
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | }
37 |
38 | templ Home(title, message string, global int) {
39 |
40 | @TypingEffect(title, "head-title", 100)
41 |
42 | @TypingEffect(message, "message", 80)
43 |
44 |
45 | @Count(global)
46 |
more examples at
47 |
48 | hello_gotham
49 |
50 |
51 |
52 | }
53 |
54 |
55 | templ Count(global int) {
56 |
58 | Database count >>
59 | {strconv.Itoa(global)}
61 |
62 | }
63 |
--------------------------------------------------------------------------------
/handlers.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "database/sql"
5 | "errors"
6 | "fmt"
7 | "log"
8 | "net/http"
9 |
10 | "github.com/a-h/templ"
11 | )
12 |
13 | func onError(w http.ResponseWriter, err error, msg string, code int) {
14 | if err != nil {
15 | http.Error(w, msg, code)
16 | log.Println(msg, err)
17 | }
18 | }
19 |
20 | func RenderView(w http.ResponseWriter, r *http.Request, view templ.Component, layoutPath string) {
21 | if r.Header.Get("Hx-Request") == "true" {
22 | err := view.Render(r.Context(), w)
23 | onError(w, err, "Internal server error", http.StatusInternalServerError)
24 | return
25 | }
26 | err := Layout(layoutPath).Render(r.Context(), w)
27 | onError(w, err, "Internal server error", http.StatusInternalServerError)
28 | }
29 |
30 | func HomeGetHandler(w http.ResponseWriter, r *http.Request) {
31 | title := "Hello World!"
32 | msg := `start by editing this text, find it in ./handlers.go as var called "msg".`
33 |
34 | dbClient, ok := r.Context().Value(DbClientKey).(*sql.DB)
35 | if !ok {
36 | onError(w, errors.New("Couldnt retrieve dbclient from context"),
37 | "Internal server error", http.StatusInternalServerError)
38 | return
39 | }
40 | globalValues := GlobalValuesInstance{ID: 1}
41 | err := globalValues.Create(dbClient)
42 | onError(w, err, "Internal server error", http.StatusInternalServerError)
43 |
44 | err = globalValues.Read(dbClient)
45 | onError(w, err, "Internal server error", http.StatusInternalServerError)
46 |
47 | RenderView(w, r, Home(title, msg, globalValues.Count), "/")
48 | }
49 |
50 |
51 | func CountPostHandler(w http.ResponseWriter, r *http.Request) {
52 | dbClient, ok := r.Context().Value(DbClientKey).(*sql.DB)
53 | if !ok {
54 | onError(w, errors.New("Couldnt retrieve dbclient from context"),
55 | "Internal server error", http.StatusInternalServerError)
56 | return
57 | }
58 |
59 | globalValues := GlobalValuesInstance{ID: 1}
60 | err := globalValues.Read(dbClient)
61 | onError(w, err, "Internal server error", http.StatusInternalServerError)
62 |
63 | err = globalValues.Update(dbClient)
64 | onError(w, err, "Internal server error", http.StatusInternalServerError)
65 |
66 | w.Header().Set("Content-Type", "text/plain")
67 | fmt.Fprint(w, globalValues.Count)
68 | }
69 |
--------------------------------------------------------------------------------
/global.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "database/sql"
5 | "fmt"
6 | )
7 |
8 | type GlobalValuesInstance struct {
9 | ID int
10 | Count int
11 | }
12 |
13 | const GlobalValuesTableKey string = "global_values"
14 |
15 | func InitGlobalValuesTable(dbClient *sql.DB) error {
16 | query := fmt.Sprintf("SHOW TABLES LIKE '%s';", GlobalValuesTableKey)
17 | rows, err := dbClient.Query(query); if err != nil {return err}
18 | defer rows.Close()
19 |
20 | if !rows.Next() {
21 | query := fmt.Sprintf(`
22 | CREATE TABLE %s (
23 | id INT AUTO_INCREMENT PRIMARY KEY,
24 | count INT NOT NULL
25 | );
26 | `, GlobalValuesTableKey)
27 |
28 | _, err := dbClient.Exec(query)
29 | return err
30 | }
31 | return nil
32 | }
33 |
34 | func (i *GlobalValuesInstance) Create(dbClient *sql.DB) error {
35 | if i.ID != 0 {
36 | query := fmt.Sprintf(`
37 | INSERT IGNORE INTO %s (id, count)
38 | VALUES (?, ?);
39 | `, GlobalValuesTableKey)
40 | _, err := dbClient.Exec(query, i.ID, i.Count)
41 | if err != nil {
42 | return err
43 | }
44 | return nil
45 | }
46 |
47 | query := fmt.Sprintf(`
48 | INSERT INTO %s (count)
49 | VALUES (?);
50 | `, GlobalValuesTableKey)
51 |
52 | _, err := dbClient.Exec(query, i.Count)
53 | if err != nil {
54 | return err
55 | }
56 | return nil
57 | }
58 |
59 | func (i *GlobalValuesInstance) Read(dbClient *sql.DB) error {
60 | query := fmt.Sprintf(`
61 | SELECT count FROM %s WHERE id=?;
62 | `, GlobalValuesTableKey)
63 |
64 | err := dbClient.QueryRow(query, i.ID).Scan(&i.Count)
65 | if err != nil {
66 | return err
67 | }
68 |
69 | return nil
70 | }
71 |
72 | func (i *GlobalValuesInstance) Update(dbClient *sql.DB) error {
73 | i.Count++
74 | query := fmt.Sprintf(`
75 | UPDATE %s
76 | SET count=?
77 | WHERE id=?;
78 | `, GlobalValuesTableKey)
79 |
80 | _, err := dbClient.Exec(query,i.Count, i.ID)
81 | if err != nil {
82 | return err
83 | }
84 | return nil
85 | }
86 |
87 |
88 | func (i *GlobalValuesInstance) Delete(dbClient *sql.DB) error {
89 | query := fmt.Sprintf(`
90 | DELETE FROM %s
91 | WHERE id=?;
92 | `, GlobalValuesTableKey)
93 |
94 | _, err := dbClient.Exec(query, i.ID)
95 | if err != nil {
96 | return err
97 | }
98 | return nil
99 | }
100 |
101 |
--------------------------------------------------------------------------------
/LICENCE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/static/css/styles.css:
--------------------------------------------------------------------------------
1 | /*
2 | ! tailwindcss v3.3.5 | MIT License | https://tailwindcss.com
3 | */
4 |
5 | /*
6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
8 | */
9 |
10 | *,
11 | ::before,
12 | ::after {
13 | box-sizing: border-box;
14 | /* 1 */
15 | border-width: 0;
16 | /* 2 */
17 | border-style: solid;
18 | /* 2 */
19 | border-color: #e5e7eb;
20 | /* 2 */
21 | }
22 |
23 | ::before,
24 | ::after {
25 | --tw-content: '';
26 | }
27 |
28 | /*
29 | 1. Use a consistent sensible line-height in all browsers.
30 | 2. Prevent adjustments of font size after orientation changes in iOS.
31 | 3. Use a more readable tab size.
32 | 4. Use the user's configured `sans` font-family by default.
33 | 5. Use the user's configured `sans` font-feature-settings by default.
34 | 6. Use the user's configured `sans` font-variation-settings by default.
35 | */
36 |
37 | html {
38 | line-height: 1.5;
39 | /* 1 */
40 | -webkit-text-size-adjust: 100%;
41 | /* 2 */
42 | -moz-tab-size: 4;
43 | /* 3 */
44 | -o-tab-size: 4;
45 | tab-size: 4;
46 | /* 3 */
47 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
48 | /* 4 */
49 | font-feature-settings: normal;
50 | /* 5 */
51 | font-variation-settings: normal;
52 | /* 6 */
53 | }
54 |
55 | /*
56 | 1. Remove the margin in all browsers.
57 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
58 | */
59 |
60 | body {
61 | margin: 0;
62 | /* 1 */
63 | line-height: inherit;
64 | /* 2 */
65 | }
66 |
67 | /*
68 | 1. Add the correct height in Firefox.
69 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
70 | 3. Ensure horizontal rules are visible by default.
71 | */
72 |
73 | hr {
74 | height: 0;
75 | /* 1 */
76 | color: inherit;
77 | /* 2 */
78 | border-top-width: 1px;
79 | /* 3 */
80 | }
81 |
82 | /*
83 | Add the correct text decoration in Chrome, Edge, and Safari.
84 | */
85 |
86 | abbr:where([title]) {
87 | -webkit-text-decoration: underline dotted;
88 | text-decoration: underline dotted;
89 | }
90 |
91 | /*
92 | Remove the default font size and weight for headings.
93 | */
94 |
95 | h1,
96 | h2,
97 | h3,
98 | h4,
99 | h5,
100 | h6 {
101 | font-size: inherit;
102 | font-weight: inherit;
103 | }
104 |
105 | /*
106 | Reset links to optimize for opt-in styling instead of opt-out.
107 | */
108 |
109 | a {
110 | color: inherit;
111 | text-decoration: inherit;
112 | }
113 |
114 | /*
115 | Add the correct font weight in Edge and Safari.
116 | */
117 |
118 | b,
119 | strong {
120 | font-weight: bolder;
121 | }
122 |
123 | /*
124 | 1. Use the user's configured `mono` font family by default.
125 | 2. Correct the odd `em` font sizing in all browsers.
126 | */
127 |
128 | code,
129 | kbd,
130 | samp,
131 | pre {
132 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
133 | /* 1 */
134 | font-size: 1em;
135 | /* 2 */
136 | }
137 |
138 | /*
139 | Add the correct font size in all browsers.
140 | */
141 |
142 | small {
143 | font-size: 80%;
144 | }
145 |
146 | /*
147 | Prevent `sub` and `sup` elements from affecting the line height in all browsers.
148 | */
149 |
150 | sub,
151 | sup {
152 | font-size: 75%;
153 | line-height: 0;
154 | position: relative;
155 | vertical-align: baseline;
156 | }
157 |
158 | sub {
159 | bottom: -0.25em;
160 | }
161 |
162 | sup {
163 | top: -0.5em;
164 | }
165 |
166 | /*
167 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
168 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
169 | 3. Remove gaps between table borders by default.
170 | */
171 |
172 | table {
173 | text-indent: 0;
174 | /* 1 */
175 | border-color: inherit;
176 | /* 2 */
177 | border-collapse: collapse;
178 | /* 3 */
179 | }
180 |
181 | /*
182 | 1. Change the font styles in all browsers.
183 | 2. Remove the margin in Firefox and Safari.
184 | 3. Remove default padding in all browsers.
185 | */
186 |
187 | button,
188 | input,
189 | optgroup,
190 | select,
191 | textarea {
192 | font-family: inherit;
193 | /* 1 */
194 | font-feature-settings: inherit;
195 | /* 1 */
196 | font-variation-settings: inherit;
197 | /* 1 */
198 | font-size: 100%;
199 | /* 1 */
200 | font-weight: inherit;
201 | /* 1 */
202 | line-height: inherit;
203 | /* 1 */
204 | color: inherit;
205 | /* 1 */
206 | margin: 0;
207 | /* 2 */
208 | padding: 0;
209 | /* 3 */
210 | }
211 |
212 | /*
213 | Remove the inheritance of text transform in Edge and Firefox.
214 | */
215 |
216 | button,
217 | select {
218 | text-transform: none;
219 | }
220 |
221 | /*
222 | 1. Correct the inability to style clickable types in iOS and Safari.
223 | 2. Remove default button styles.
224 | */
225 |
226 | button,
227 | [type='button'],
228 | [type='reset'],
229 | [type='submit'] {
230 | -webkit-appearance: button;
231 | /* 1 */
232 | background-color: transparent;
233 | /* 2 */
234 | background-image: none;
235 | /* 2 */
236 | }
237 |
238 | /*
239 | Use the modern Firefox focus style for all focusable elements.
240 | */
241 |
242 | :-moz-focusring {
243 | outline: auto;
244 | }
245 |
246 | /*
247 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
248 | */
249 |
250 | :-moz-ui-invalid {
251 | box-shadow: none;
252 | }
253 |
254 | /*
255 | Add the correct vertical alignment in Chrome and Firefox.
256 | */
257 |
258 | progress {
259 | vertical-align: baseline;
260 | }
261 |
262 | /*
263 | Correct the cursor style of increment and decrement buttons in Safari.
264 | */
265 |
266 | ::-webkit-inner-spin-button,
267 | ::-webkit-outer-spin-button {
268 | height: auto;
269 | }
270 |
271 | /*
272 | 1. Correct the odd appearance in Chrome and Safari.
273 | 2. Correct the outline style in Safari.
274 | */
275 |
276 | [type='search'] {
277 | -webkit-appearance: textfield;
278 | /* 1 */
279 | outline-offset: -2px;
280 | /* 2 */
281 | }
282 |
283 | /*
284 | Remove the inner padding in Chrome and Safari on macOS.
285 | */
286 |
287 | ::-webkit-search-decoration {
288 | -webkit-appearance: none;
289 | }
290 |
291 | /*
292 | 1. Correct the inability to style clickable types in iOS and Safari.
293 | 2. Change font properties to `inherit` in Safari.
294 | */
295 |
296 | ::-webkit-file-upload-button {
297 | -webkit-appearance: button;
298 | /* 1 */
299 | font: inherit;
300 | /* 2 */
301 | }
302 |
303 | /*
304 | Add the correct display in Chrome and Safari.
305 | */
306 |
307 | summary {
308 | display: list-item;
309 | }
310 |
311 | /*
312 | Removes the default spacing and border for appropriate elements.
313 | */
314 |
315 | blockquote,
316 | dl,
317 | dd,
318 | h1,
319 | h2,
320 | h3,
321 | h4,
322 | h5,
323 | h6,
324 | hr,
325 | figure,
326 | p,
327 | pre {
328 | margin: 0;
329 | }
330 |
331 | fieldset {
332 | margin: 0;
333 | padding: 0;
334 | }
335 |
336 | legend {
337 | padding: 0;
338 | }
339 |
340 | ol,
341 | ul,
342 | menu {
343 | list-style: none;
344 | margin: 0;
345 | padding: 0;
346 | }
347 |
348 | /*
349 | Reset default styling for dialogs.
350 | */
351 |
352 | dialog {
353 | padding: 0;
354 | }
355 |
356 | /*
357 | Prevent resizing textareas horizontally by default.
358 | */
359 |
360 | textarea {
361 | resize: vertical;
362 | }
363 |
364 | /*
365 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
366 | 2. Set the default placeholder color to the user's configured gray 400 color.
367 | */
368 |
369 | input::-moz-placeholder, textarea::-moz-placeholder {
370 | opacity: 1;
371 | /* 1 */
372 | color: #9ca3af;
373 | /* 2 */
374 | }
375 |
376 | input::placeholder,
377 | textarea::placeholder {
378 | opacity: 1;
379 | /* 1 */
380 | color: #9ca3af;
381 | /* 2 */
382 | }
383 |
384 | /*
385 | Set the default cursor for buttons.
386 | */
387 |
388 | button,
389 | [role="button"] {
390 | cursor: pointer;
391 | }
392 |
393 | /*
394 | Make sure disabled buttons don't get the pointer cursor.
395 | */
396 |
397 | :disabled {
398 | cursor: default;
399 | }
400 |
401 | /*
402 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
403 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
404 | This can trigger a poorly considered lint error in some tools but is included by design.
405 | */
406 |
407 | img,
408 | svg,
409 | video,
410 | canvas,
411 | audio,
412 | iframe,
413 | embed,
414 | object {
415 | display: block;
416 | /* 1 */
417 | vertical-align: middle;
418 | /* 2 */
419 | }
420 |
421 | /*
422 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
423 | */
424 |
425 | img,
426 | video {
427 | max-width: 100%;
428 | height: auto;
429 | }
430 |
431 | /* Make elements with the HTML hidden attribute stay hidden by default */
432 |
433 | [hidden] {
434 | display: none;
435 | }
436 |
437 | *, ::before, ::after {
438 | --tw-border-spacing-x: 0;
439 | --tw-border-spacing-y: 0;
440 | --tw-translate-x: 0;
441 | --tw-translate-y: 0;
442 | --tw-rotate: 0;
443 | --tw-skew-x: 0;
444 | --tw-skew-y: 0;
445 | --tw-scale-x: 1;
446 | --tw-scale-y: 1;
447 | --tw-pan-x: ;
448 | --tw-pan-y: ;
449 | --tw-pinch-zoom: ;
450 | --tw-scroll-snap-strictness: proximity;
451 | --tw-gradient-from-position: ;
452 | --tw-gradient-via-position: ;
453 | --tw-gradient-to-position: ;
454 | --tw-ordinal: ;
455 | --tw-slashed-zero: ;
456 | --tw-numeric-figure: ;
457 | --tw-numeric-spacing: ;
458 | --tw-numeric-fraction: ;
459 | --tw-ring-inset: ;
460 | --tw-ring-offset-width: 0px;
461 | --tw-ring-offset-color: #fff;
462 | --tw-ring-color: rgb(59 130 246 / 0.5);
463 | --tw-ring-offset-shadow: 0 0 #0000;
464 | --tw-ring-shadow: 0 0 #0000;
465 | --tw-shadow: 0 0 #0000;
466 | --tw-shadow-colored: 0 0 #0000;
467 | --tw-blur: ;
468 | --tw-brightness: ;
469 | --tw-contrast: ;
470 | --tw-grayscale: ;
471 | --tw-hue-rotate: ;
472 | --tw-invert: ;
473 | --tw-saturate: ;
474 | --tw-sepia: ;
475 | --tw-drop-shadow: ;
476 | --tw-backdrop-blur: ;
477 | --tw-backdrop-brightness: ;
478 | --tw-backdrop-contrast: ;
479 | --tw-backdrop-grayscale: ;
480 | --tw-backdrop-hue-rotate: ;
481 | --tw-backdrop-invert: ;
482 | --tw-backdrop-opacity: ;
483 | --tw-backdrop-saturate: ;
484 | --tw-backdrop-sepia: ;
485 | }
486 |
487 | ::backdrop {
488 | --tw-border-spacing-x: 0;
489 | --tw-border-spacing-y: 0;
490 | --tw-translate-x: 0;
491 | --tw-translate-y: 0;
492 | --tw-rotate: 0;
493 | --tw-skew-x: 0;
494 | --tw-skew-y: 0;
495 | --tw-scale-x: 1;
496 | --tw-scale-y: 1;
497 | --tw-pan-x: ;
498 | --tw-pan-y: ;
499 | --tw-pinch-zoom: ;
500 | --tw-scroll-snap-strictness: proximity;
501 | --tw-gradient-from-position: ;
502 | --tw-gradient-via-position: ;
503 | --tw-gradient-to-position: ;
504 | --tw-ordinal: ;
505 | --tw-slashed-zero: ;
506 | --tw-numeric-figure: ;
507 | --tw-numeric-spacing: ;
508 | --tw-numeric-fraction: ;
509 | --tw-ring-inset: ;
510 | --tw-ring-offset-width: 0px;
511 | --tw-ring-offset-color: #fff;
512 | --tw-ring-color: rgb(59 130 246 / 0.5);
513 | --tw-ring-offset-shadow: 0 0 #0000;
514 | --tw-ring-shadow: 0 0 #0000;
515 | --tw-shadow: 0 0 #0000;
516 | --tw-shadow-colored: 0 0 #0000;
517 | --tw-blur: ;
518 | --tw-brightness: ;
519 | --tw-contrast: ;
520 | --tw-grayscale: ;
521 | --tw-hue-rotate: ;
522 | --tw-invert: ;
523 | --tw-saturate: ;
524 | --tw-sepia: ;
525 | --tw-drop-shadow: ;
526 | --tw-backdrop-blur: ;
527 | --tw-backdrop-brightness: ;
528 | --tw-backdrop-contrast: ;
529 | --tw-backdrop-grayscale: ;
530 | --tw-backdrop-hue-rotate: ;
531 | --tw-backdrop-invert: ;
532 | --tw-backdrop-opacity: ;
533 | --tw-backdrop-saturate: ;
534 | --tw-backdrop-sepia: ;
535 | }
536 |
537 | .flex {
538 | display: flex;
539 | }
540 |
541 | .h-screen {
542 | height: 100vh;
543 | }
544 |
545 | .w-1\/4 {
546 | width: 25%;
547 | }
548 |
549 | .w-screen {
550 | width: 100vw;
551 | }
552 |
553 | .cursor-pointer {
554 | cursor: pointer;
555 | }
556 |
557 | .select-none {
558 | -webkit-user-select: none;
559 | -moz-user-select: none;
560 | user-select: none;
561 | }
562 |
563 | .flex-col {
564 | flex-direction: column;
565 | }
566 |
567 | .items-center {
568 | align-items: center;
569 | }
570 |
571 | .justify-center {
572 | justify-content: center;
573 | }
574 |
575 | .gap-8 {
576 | gap: 2rem;
577 | }
578 |
579 | .bg-slate-900 {
580 | --tw-bg-opacity: 1;
581 | background-color: rgb(15 23 42 / var(--tw-bg-opacity));
582 | }
583 |
584 | .p-4 {
585 | padding: 1rem;
586 | }
587 |
588 | .py-2 {
589 | padding-top: 0.5rem;
590 | padding-bottom: 0.5rem;
591 | }
592 |
593 | .text-center {
594 | text-align: center;
595 | }
596 |
597 | .font-mono {
598 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
599 | }
600 |
601 | .text-4xl {
602 | font-size: 2.25rem;
603 | line-height: 2.5rem;
604 | }
605 |
606 | .text-5xl {
607 | font-size: 3rem;
608 | line-height: 1;
609 | }
610 |
611 | .text-base {
612 | font-size: 1rem;
613 | line-height: 1.5rem;
614 | }
615 |
616 | .text-sm {
617 | font-size: 0.875rem;
618 | line-height: 1.25rem;
619 | }
620 |
621 | .font-bold {
622 | font-weight: 700;
623 | }
624 |
625 | .italic {
626 | font-style: italic;
627 | }
628 |
629 | .text-gray-100 {
630 | --tw-text-opacity: 1;
631 | color: rgb(243 244 246 / var(--tw-text-opacity));
632 | }
633 |
634 | .text-sky-500 {
635 | --tw-text-opacity: 1;
636 | color: rgb(14 165 233 / var(--tw-text-opacity));
637 | }
638 |
639 | .text-slate-300 {
640 | --tw-text-opacity: 1;
641 | color: rgb(203 213 225 / var(--tw-text-opacity));
642 | }
643 |
644 | .text-yellow-400 {
645 | --tw-text-opacity: 1;
646 | color: rgb(250 204 21 / var(--tw-text-opacity));
647 | }
648 |
649 | .opacity-60 {
650 | opacity: 0.6;
651 | }
652 |
653 | .button {
654 | cursor: pointer;
655 | -webkit-user-select: none;
656 | -moz-user-select: none;
657 | user-select: none;
658 | border-radius: 0.5rem;
659 | --tw-bg-opacity: 1;
660 | background-color: rgb(59 130 246 / var(--tw-bg-opacity));
661 | padding: 0.5rem;
662 | text-align: center;
663 | font-weight: 700;
664 | text-transform: uppercase;
665 | --tw-text-opacity: 1;
666 | color: rgb(100 116 139 / var(--tw-text-opacity));
667 | }
668 |
669 | .button:hover {
670 | --tw-bg-opacity: 1;
671 | background-color: rgb(147 197 253 / var(--tw-bg-opacity));
672 | }
673 |
674 | .button:active {
675 | --tw-bg-opacity: 1;
676 | background-color: rgb(34 197 94 / var(--tw-bg-opacity));
677 | }
678 |
679 | .hover\:text-sky-300:hover {
680 | --tw-text-opacity: 1;
681 | color: rgb(125 211 252 / var(--tw-text-opacity));
682 | }
683 |
684 | .hover\:opacity-100:hover {
685 | opacity: 1;
686 | }
687 |
--------------------------------------------------------------------------------
/static/js/htmx.min.js:
--------------------------------------------------------------------------------
1 | (function(e,t){if(typeof define==="function"&&define.amd){define([],t)}else if(typeof module==="object"&&module.exports){module.exports=t()}else{e.htmx=e.htmx||t()}})(typeof self!=="undefined"?self:this,function(){return function(){"use strict";var Q={onLoad:t,process:Bt,on:Z,off:K,trigger:ce,ajax:Or,find:C,findAll:f,closest:v,values:function(e,t){var r=ur(e,t||"post");return r.values},remove:B,addClass:F,removeClass:n,toggleClass:V,takeClass:j,defineExtension:kr,removeExtension:Pr,logAll:X,logNone:U,logger:null,config:{historyEnabled:true,historyCacheSize:10,refreshOnHistoryMiss:false,defaultSwapStyle:"innerHTML",defaultSwapDelay:0,defaultSettleDelay:20,includeIndicatorStyles:true,indicatorClass:"htmx-indicator",requestClass:"htmx-request",addedClass:"htmx-added",settlingClass:"htmx-settling",swappingClass:"htmx-swapping",allowEval:true,allowScriptTags:true,inlineScriptNonce:"",attributesToSettle:["class","style","width","height"],withCredentials:false,timeout:0,wsReconnectDelay:"full-jitter",wsBinaryType:"blob",disableSelector:"[hx-disable], [data-hx-disable]",useTemplateFragments:false,scrollBehavior:"smooth",defaultFocusScroll:false,getCacheBusterParam:false,globalViewTransitions:false,methodsThatUseUrlParams:["get"],selfRequestsOnly:false,ignoreTitle:false,scrollIntoViewOnBoost:true},parseInterval:d,_:e,createEventSource:function(e){return new EventSource(e,{withCredentials:true})},createWebSocket:function(e){var t=new WebSocket(e,[]);t.binaryType=Q.config.wsBinaryType;return t},version:"1.9.9"};var r={addTriggerHandler:Tt,bodyContains:se,canAccessLocalStorage:M,findThisElement:de,filterValues:dr,hasAttribute:o,getAttributeValue:te,getClosestAttributeValue:ne,getClosestMatch:c,getExpressionVars:Cr,getHeaders:vr,getInputValues:ur,getInternalData:ae,getSwapSpecification:mr,getTriggerSpecs:Qe,getTarget:ge,makeFragment:l,mergeObjects:le,makeSettleInfo:R,oobSwap:xe,querySelectorExt:ue,selectAndSwap:Ue,settleImmediately:Yt,shouldCancel:it,triggerEvent:ce,triggerErrorEvent:fe,withExtensions:T};var b=["get","post","put","delete","patch"];var w=b.map(function(e){return"[hx-"+e+"], [data-hx-"+e+"]"}).join(", ");function d(e){if(e==undefined){return undefined}if(e.slice(-2)=="ms"){return parseFloat(e.slice(0,-2))||undefined}if(e.slice(-1)=="s"){return parseFloat(e.slice(0,-1))*1e3||undefined}if(e.slice(-1)=="m"){return parseFloat(e.slice(0,-1))*1e3*60||undefined}return parseFloat(e)||undefined}function ee(e,t){return e.getAttribute&&e.getAttribute(t)}function o(e,t){return e.hasAttribute&&(e.hasAttribute(t)||e.hasAttribute("data-"+t))}function te(e,t){return ee(e,t)||ee(e,"data-"+t)}function u(e){return e.parentElement}function re(){return document}function c(e,t){while(e&&!t(e)){e=u(e)}return e?e:null}function S(e,t,r){var n=te(t,r);var i=te(t,"hx-disinherit");if(e!==t&&i&&(i==="*"||i.split(" ").indexOf(r)>=0)){return"unset"}else{return n}}function ne(t,r){var n=null;c(t,function(e){return n=S(t,e,r)});if(n!=="unset"){return n}}function h(e,t){var r=e.matches||e.matchesSelector||e.msMatchesSelector||e.mozMatchesSelector||e.webkitMatchesSelector||e.oMatchesSelector;return r&&r.call(e,t)}function q(e){var t=/<([a-z][^\/\0>\x20\t\r\n\f]*)/i;var r=t.exec(e);if(r){return r[1].toLowerCase()}else{return""}}function i(e,t){var r=new DOMParser;var n=r.parseFromString(e,"text/html");var i=n.body;while(t>0){t--;i=i.firstChild}if(i==null){i=re().createDocumentFragment()}return i}function H(e){return e.match(/"+e+"",0);return r.querySelector("template").content}else{var n=q(e);switch(n){case"thead":case"tbody":case"tfoot":case"colgroup":case"caption":return i("",1);case"col":return i("",2);case"tr":return i("",2);case"td":case"th":return i("",3);case"script":case"style":return i(""+e+"
",1);default:return i(e,0)}}}function ie(e){if(e){e()}}function L(e,t){return Object.prototype.toString.call(e)==="[object "+t+"]"}function A(e){return L(e,"Function")}function N(e){return L(e,"Object")}function ae(e){var t="htmx-internal-data";var r=e[t];if(!r){r=e[t]={}}return r}function I(e){var t=[];if(e){for(var r=0;r=0}function se(e){if(e.getRootNode&&e.getRootNode()instanceof window.ShadowRoot){return re().body.contains(e.getRootNode().host)}else{return re().body.contains(e)}}function P(e){return e.trim().split(/\s+/)}function le(e,t){for(var r in t){if(t.hasOwnProperty(r)){e[r]=t[r]}}return e}function E(e){try{return JSON.parse(e)}catch(e){x(e);return null}}function M(){var e="htmx:localStorageTest";try{localStorage.setItem(e,e);localStorage.removeItem(e);return true}catch(e){return false}}function D(t){try{var e=new URL(t);if(e){t=e.pathname+e.search}if(!t.match("^/$")){t=t.replace(/\/+$/,"")}return t}catch(e){return t}}function e(e){return wr(re().body,function(){return eval(e)})}function t(t){var e=Q.on("htmx:load",function(e){t(e.detail.elt)});return e}function X(){Q.logger=function(e,t,r){if(console){console.log(t,e,r)}}}function U(){Q.logger=null}function C(e,t){if(t){return e.querySelector(t)}else{return C(re(),e)}}function f(e,t){if(t){return e.querySelectorAll(t)}else{return f(re(),e)}}function B(e,t){e=s(e);if(t){setTimeout(function(){B(e);e=null},t)}else{e.parentElement.removeChild(e)}}function F(e,t,r){e=s(e);if(r){setTimeout(function(){F(e,t);e=null},r)}else{e.classList&&e.classList.add(t)}}function n(e,t,r){e=s(e);if(r){setTimeout(function(){n(e,t);e=null},r)}else{if(e.classList){e.classList.remove(t);if(e.classList.length===0){e.removeAttribute("class")}}}}function V(e,t){e=s(e);e.classList.toggle(t)}function j(e,t){e=s(e);oe(e.parentElement.children,function(e){n(e,t)});F(e,t)}function v(e,t){e=s(e);if(e.closest){return e.closest(t)}else{do{if(e==null||h(e,t)){return e}}while(e=e&&u(e));return null}}function g(e,t){return e.substring(0,t.length)===t}function _(e,t){return e.substring(e.length-t.length)===t}function z(e){var t=e.trim();if(g(t,"<")&&_(t,"/>")){return t.substring(1,t.length-2)}else{return t}}function W(e,t){if(t.indexOf("closest ")===0){return[v(e,z(t.substr(8)))]}else if(t.indexOf("find ")===0){return[C(e,z(t.substr(5)))]}else if(t==="next"){return[e.nextElementSibling]}else if(t.indexOf("next ")===0){return[$(e,z(t.substr(5)))]}else if(t==="previous"){return[e.previousElementSibling]}else if(t.indexOf("previous ")===0){return[G(e,z(t.substr(9)))]}else if(t==="document"){return[document]}else if(t==="window"){return[window]}else if(t==="body"){return[document.body]}else{return re().querySelectorAll(z(t))}}var $=function(e,t){var r=re().querySelectorAll(t);for(var n=0;n=0;n--){var i=r[n];if(i.compareDocumentPosition(e)===Node.DOCUMENT_POSITION_FOLLOWING){return i}}};function ue(e,t){if(t){return W(e,t)[0]}else{return W(re().body,e)[0]}}function s(e){if(L(e,"String")){return C(e)}else{return e}}function J(e,t,r){if(A(t)){return{target:re().body,event:e,listener:t}}else{return{target:s(e),event:t,listener:r}}}function Z(t,r,n){Dr(function(){var e=J(t,r,n);e.target.addEventListener(e.event,e.listener)});var e=A(r);return e?r:n}function K(t,r,n){Dr(function(){var e=J(t,r,n);e.target.removeEventListener(e.event,e.listener)});return A(r)?r:n}var ve=re().createElement("output");function Y(e,t){var r=ne(e,t);if(r){if(r==="this"){return[de(e,t)]}else{var n=W(e,r);if(n.length===0){x('The selector "'+r+'" on '+t+" returned no matches!");return[ve]}else{return n}}}}function de(e,t){return c(e,function(e){return te(e,t)!=null})}function ge(e){var t=ne(e,"hx-target");if(t){if(t==="this"){return de(e,"hx-target")}else{return ue(e,t)}}else{var r=ae(e);if(r.boosted){return re().body}else{return e}}}function me(e){var t=Q.config.attributesToSettle;for(var r=0;r0){o=e.substr(0,e.indexOf(":"));t=e.substr(e.indexOf(":")+1,e.length)}else{o=e}var r=re().querySelectorAll(t);if(r){oe(r,function(e){var t;var r=i.cloneNode(true);t=re().createDocumentFragment();t.appendChild(r);if(!ye(o,e)){t=r}var n={shouldSwap:true,target:e,fragment:t};if(!ce(e,"htmx:oobBeforeSwap",n))return;e=n.target;if(n["shouldSwap"]){De(o,e,e,t,a)}oe(a.elts,function(e){ce(e,"htmx:oobAfterSwap",n)})});i.parentNode.removeChild(i)}else{i.parentNode.removeChild(i);fe(re().body,"htmx:oobErrorNoTarget",{content:i})}return e}function be(e,t,r){var n=ne(e,"hx-select-oob");if(n){var i=n.split(",");for(let e=0;e0){var r=t.replace("'","\\'");var n=e.tagName.replace(":","\\:");var i=o.querySelector(n+"[id='"+r+"']");if(i&&i!==o){var a=e.cloneNode();pe(e,i);s.tasks.push(function(){pe(e,a)})}}})}function Ee(e){return function(){n(e,Q.config.addedClass);Bt(e);Ot(e);Ce(e);ce(e,"htmx:load")}}function Ce(e){var t="[autofocus]";var r=h(e,t)?e:e.querySelector(t);if(r!=null){r.focus()}}function a(e,t,r,n){Se(e,r,n);while(r.childNodes.length>0){var i=r.firstChild;F(i,Q.config.addedClass);e.insertBefore(i,t);if(i.nodeType!==Node.TEXT_NODE&&i.nodeType!==Node.COMMENT_NODE){n.tasks.push(Ee(i))}}}function Te(e,t){var r=0;while(r-1){var t=e.replace(/