├── .gitignore ├── main.db ├── postcss.config.js ├── cmd └── main.go ├── Makefile ├── views ├── components │ ├── Hero_templ.txt │ ├── Hero.templ │ ├── Header_templ.txt │ ├── Header.templ │ ├── Hero_templ.go │ └── Header_templ.go ├── layouts │ ├── base_templ.txt │ ├── base.templ │ └── base_templ.go ├── pages │ ├── login_templ.txt │ ├── index_templ.txt │ ├── login.templ │ ├── index.templ │ ├── login_templ.go │ └── index_templ.go └── css │ └── main.css ├── tailwind.config.js ├── package.json ├── go.mod ├── internal ├── entities │ └── todo.go ├── storage │ └── sqlite │ │ ├── user.go │ │ ├── new.go │ │ └── todo.go ├── transport │ └── web │ │ ├── new.go │ │ ├── utils.go │ │ └── handlers.go └── services │ └── new.go ├── .air.toml ├── go.sum └── public └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | tmp/ -------------------------------------------------------------------------------- /main.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethanhamilthon/gohtmx_boiler/main/main.db -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "templtest/internal/transport/web" 4 | 5 | func main() { 6 | server := web.New() 7 | server.Serve() 8 | } 9 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | run: 2 | ./runner.sh 3 | 4 | css: 5 | npx tailwindcss build -i views/css/main.css -o public/style.css --watch 6 | 7 | templ: 8 | templ generate --watch --proxy="http://localhost:4000" --open-browser=false 9 | -------------------------------------------------------------------------------- /views/components/Hero_templ.txt: -------------------------------------------------------------------------------- 1 |

2 |

3 | -------------------------------------------------------------------------------- /views/components/Hero.templ: -------------------------------------------------------------------------------- 1 | package components 2 | 3 | templ Hero(heading string){ 4 |
5 |

{heading}

6 |

7 |
8 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./views/**/*.{html,js,go,templ}"], 4 | theme: { 5 | container: { 6 | center: true, 7 | padding: "36px", 8 | screens: { 9 | "2xl": "1200px", 10 | }, 11 | }, 12 | }, 13 | plugins: [], 14 | }; 15 | -------------------------------------------------------------------------------- /views/components/Header_templ.txt: -------------------------------------------------------------------------------- 1 |

SyncWord

2 |
3 |
4 | 5 |
6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "templ_test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "autoprefixer": "^10.4.19", 14 | "postcss": "^8.4.39", 15 | "tailwindcss": "^3.4.4" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module templtest 2 | 3 | go 1.22.1 4 | 5 | require ( 6 | github.com/Masterminds/squirrel v1.5.4 // indirect 7 | github.com/a-h/templ v0.2.747 // indirect 8 | github.com/golang-jwt/jwt/v5 v5.2.1 // indirect 9 | github.com/google/uuid v1.6.0 // indirect 10 | github.com/jmoiron/sqlx v1.4.0 // indirect 11 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect 12 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect 13 | github.com/mattn/go-sqlite3 v1.14.22 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /views/layouts/base_templ.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /internal/entities/todo.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | type Todo struct { 4 | ID string `json:"id" db:"id"` 5 | Title string `json:"title" db:"title"` 6 | IsCompleted string `json:"is_completed" db:"is_completed"` 7 | UserID string `json:"user_id" db:"user_id"` 8 | CreatedAt string `json:"created_at" db:"created_at"` 9 | } 10 | 11 | type User struct { 12 | ID string `json:"id" db:"id"` 13 | Name string `json:"name" db:"name"` 14 | Email string `json:"email" db:"email"` 15 | Password string `json:"password" db:"password"` 16 | CreatedAt string `json:"created_at" db:"created_at"` 17 | } 18 | -------------------------------------------------------------------------------- /views/components/Header.templ: -------------------------------------------------------------------------------- 1 | package components 2 | import ( 3 | "templtest/internal/entities" 4 | ) 5 | 6 | 7 | templ Header(user entities.User){ 8 |
9 |
10 |

SyncWord

11 | @Profile(user.Name, user.Email) 12 |
13 |
14 | } 15 | 16 | templ Profile(userName, userEmail string){ 17 |
18 | {userName} 19 | {userEmail} 20 |
21 | } -------------------------------------------------------------------------------- /views/layouts/base.templ: -------------------------------------------------------------------------------- 1 | package layouts 2 | 3 | templ BaseLayout(title string) { 4 | 5 | 6 | 7 | 8 | 9 | {title} 10 | 11 | 12 | 13 | 14 | 15 | {children...} 16 | 17 | 18 | } 19 | 20 | -------------------------------------------------------------------------------- /views/pages/login_templ.txt: -------------------------------------------------------------------------------- 1 |

Welcome to system

2 | 3 | 4 |
5 | -------------------------------------------------------------------------------- /internal/storage/sqlite/user.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "templtest/internal/entities" 5 | 6 | sq "github.com/Masterminds/squirrel" 7 | ) 8 | 9 | func (repo *Repository) GetUser(Email string) (entities.User, error) { 10 | var user entities.User 11 | 12 | query, args, err := sq.Select("*"). 13 | From("users"). 14 | Where(sq.Eq{"email": Email}). 15 | ToSql() 16 | if err != nil { 17 | return user, err 18 | } 19 | 20 | err = repo.db.Get(&user, query, args...) 21 | if err != nil { 22 | return user, err 23 | } 24 | 25 | return user, nil 26 | } 27 | 28 | func (repo *Repository) CreateUser(user entities.User) error { 29 | 30 | query, args, err := sq.Insert("users"). 31 | Columns("id", "email", "name", "password"). 32 | Values(user.ID, user.Email, user.Name, user.Password). 33 | ToSql() 34 | if err != nil { 35 | return err 36 | } 37 | 38 | _, err = repo.db.Exec(query, args...) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /views/css/main.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @keyframes fade-in { 6 | from { 7 | opacity: 0; 8 | } 9 | } 10 | 11 | @keyframes fade-out { 12 | to { 13 | opacity: 0; 14 | } 15 | } 16 | 17 | @keyframes slide-from-right { 18 | from { 19 | transform: translateX(90px); 20 | } 21 | } 22 | 23 | @keyframes slide-to-left { 24 | to { 25 | transform: translateX(-90px); 26 | } 27 | } 28 | 29 | /* define animations for the old and new content */ 30 | ::view-transition-old(slide-it) { 31 | animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out, 32 | 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; 33 | } 34 | ::view-transition-new(slide-it) { 35 | animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 36 | 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; 37 | } 38 | 39 | /* tie the view transition to a given CSS class */ 40 | .sample-transition { 41 | view-transition-name: slide-it; 42 | } 43 | -------------------------------------------------------------------------------- /.air.toml: -------------------------------------------------------------------------------- 1 | root = "." 2 | testdata_dir = "testdata" 3 | tmp_dir = "tmp" 4 | 5 | [build] 6 | args_bin = [] 7 | bin = "./tmp/main" 8 | cmd = "go build -o ./tmp/main cmd/main.go" 9 | delay = 1000 10 | exclude_dir = ["assets", "tmp", "vendor", "testdata"] 11 | exclude_file = [] 12 | exclude_regex = ["_test.go"] 13 | exclude_unchanged = false 14 | follow_symlink = false 15 | full_bin = "" 16 | include_dir = [] 17 | include_ext = ["go", "tpl", "tmpl", "html"] 18 | include_file = [] 19 | kill_delay = "0s" 20 | log = "build-errors.log" 21 | poll = false 22 | poll_interval = 0 23 | post_cmd = [] 24 | pre_cmd = [] 25 | rerun = false 26 | rerun_delay = 500 27 | send_interrupt = false 28 | stop_on_error = false 29 | 30 | [color] 31 | app = "" 32 | build = "yellow" 33 | main = "magenta" 34 | runner = "green" 35 | watcher = "cyan" 36 | 37 | [log] 38 | main_only = false 39 | time = false 40 | 41 | [misc] 42 | clean_on_exit = false 43 | 44 | [screen] 45 | clear_on_rebuild = false 46 | keep_scroll = true 47 | -------------------------------------------------------------------------------- /views/pages/index_templ.txt: -------------------------------------------------------------------------------- 1 |
3 |
Пока нету Todos
4 |
5 |
6 |
9 |
10 | -------------------------------------------------------------------------------- /views/pages/login.templ: -------------------------------------------------------------------------------- 1 | package pages 2 | 3 | import "templtest/views/layouts" 4 | 5 | templ LoginPage(err string){ 6 | @layouts.BaseLayout("Go app"){ 7 |
8 |
9 |

Welcome to system

10 | if err != "" { 11 | {err} 12 | } 13 |
14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 | } 25 | 26 | } -------------------------------------------------------------------------------- /internal/storage/sqlite/new.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/jmoiron/sqlx" 7 | _ "github.com/mattn/go-sqlite3" 8 | ) 9 | 10 | type Repository struct { 11 | db *sqlx.DB 12 | } 13 | 14 | func New() (*Repository, error) { 15 | db, err := sqlx.Open("sqlite3", "./main.db") 16 | if err != nil { 17 | return &Repository{}, err 18 | } 19 | 20 | err = db.Ping() 21 | if err != nil { 22 | return &Repository{}, err 23 | } 24 | 25 | newDB := &Repository{ 26 | db: db, 27 | } 28 | err = newDB.runMigration() 29 | if err != nil { 30 | return &Repository{}, err 31 | } 32 | fmt.Println("db connected") 33 | return newDB, nil 34 | } 35 | 36 | func (repo *Repository) Close() { 37 | repo.db.Close() 38 | } 39 | 40 | func (repo *Repository) runMigration() error { 41 | _, err := repo.db.Exec(` 42 | CREATE TABLE IF NOT EXISTS todos ( 43 | id TEXT PRIMARY KEY, 44 | title TEXT, 45 | is_completed TEXT, 46 | user_id TEXT, 47 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 48 | FOREIGN KEY(user_id) REFERENCES users(id) 49 | ) 50 | `) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | // Создание таблицы languages 56 | _, err = repo.db.Exec(` 57 | CREATE TABLE IF NOT EXISTS users ( 58 | id TEXT PRIMARY KEY, 59 | email TEXT UNIQUE, 60 | name TEXT, 61 | password TEXT, 62 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 63 | ) 64 | `) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | return nil 70 | } 71 | -------------------------------------------------------------------------------- /internal/transport/web/new.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | 11 | "time" 12 | ) 13 | 14 | type Server struct { 15 | mux *http.ServeMux 16 | } 17 | 18 | func New() *Server { 19 | main := http.NewServeMux() 20 | handlers := NewHandlers() 21 | fsHandler := http.FileServer(http.Dir("./public")) 22 | main.Handle("/public/", http.StripPrefix("/public/", fsHandler)) 23 | main.HandleFunc("/", handlers.Index) 24 | main.HandleFunc("/login", handlers.Login) 25 | main.HandleFunc("POST /todos", handlers.CreateTodo) 26 | main.HandleFunc("DELETE /todos/{id}", handlers.DeleteTodo) 27 | return &Server{mux: main} 28 | } 29 | 30 | func (s *Server) Serve() { 31 | ctx, cancel := context.WithCancel(context.Background()) 32 | defer cancel() 33 | 34 | // Signal to close app 35 | osSignal := make(chan os.Signal, 1) 36 | signal.Notify(osSignal, os.Interrupt, syscall.SIGTERM) 37 | 38 | server := &http.Server{ 39 | Addr: ":4000", 40 | Handler: s.mux, 41 | } 42 | 43 | go func() { 44 | log.Printf("Starting server on port 4000") 45 | if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 46 | log.Fatalf("Server failed: %v", err) 47 | } 48 | }() 49 | 50 | select { 51 | case <-osSignal: 52 | log.Println("Shutdown signal received. Shutting down...") 53 | 54 | // Timeout to close handlers 55 | shutdownCtx, shutdownCancel := context.WithTimeout(ctx, 30*time.Second) 56 | defer shutdownCancel() 57 | 58 | // Stop the server 59 | if err := server.Shutdown(shutdownCtx); err != nil { 60 | log.Fatalf("Server shutdown failed: %v", err) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /internal/services/new.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "templtest/internal/entities" 6 | 7 | "github.com/google/uuid" 8 | ) 9 | 10 | type DB interface { 11 | CreateTodo(todo entities.Todo) error 12 | GetTodos(userID string) ([]entities.Todo, error) 13 | UpdateTodo(todo entities.Todo) error 14 | DeleteTodo(string, string) error 15 | GetUser(Email string) (entities.User, error) 16 | CreateUser(user entities.User) error 17 | } 18 | 19 | type Service struct { 20 | db DB 21 | } 22 | 23 | func New(db DB) *Service { 24 | return &Service{db} 25 | } 26 | 27 | func (s *Service) UserLogin(Email, Name, Password string) (entities.User, error) { 28 | usergetted, err := s.db.GetUser(Email) 29 | if err != nil { 30 | fmt.Println(err.Error()) 31 | user := entities.User{ 32 | ID: uuid.New().String(), 33 | Email: Email, 34 | Name: Name, 35 | Password: Password, 36 | } 37 | err = s.db.CreateUser(user) 38 | if err != nil { 39 | fmt.Println(err.Error()) 40 | return entities.User{}, err 41 | } 42 | return user, nil 43 | } 44 | return usergetted, nil 45 | } 46 | 47 | func (s *Service) User(Email string) (entities.User, error) { 48 | return s.db.GetUser(Email) 49 | } 50 | 51 | func (s *Service) Todos(UserID string) ([]entities.Todo, error) { 52 | return s.db.GetTodos(UserID) 53 | } 54 | 55 | func (s *Service) CreateTodos(Title, UserID string) (entities.Todo, error) { 56 | newTodo := entities.Todo{ 57 | ID: uuid.New().String(), 58 | Title: Title, 59 | IsCompleted: "false", 60 | UserID: UserID, 61 | } 62 | 63 | return newTodo, s.db.CreateTodo(newTodo) 64 | } 65 | 66 | func (s *Service) DeleteTodo(ID string, UserID string) error { 67 | return s.db.DeleteTodo(ID, UserID) 68 | } 69 | -------------------------------------------------------------------------------- /internal/transport/web/utils.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/golang-jwt/jwt/v5" 10 | ) 11 | 12 | func getAuthCookie(r *http.Request) (string, error) { 13 | cookies := r.Cookies() 14 | for _, cookie := range cookies { 15 | if cookie.Name == "Authorization" { 16 | return cookie.Value, nil 17 | } 18 | } 19 | 20 | return "", errors.New("Cookies not found") 21 | } 22 | 23 | type MyClaims struct { 24 | UserID string `json:"user_id"` 25 | UserEmail string `json:"user_email"` 26 | jwt.RegisteredClaims 27 | } 28 | 29 | func CreateJWT(userID string, userEmail string) (string, error) { 30 | jwtKey := []byte("some-key") 31 | expirationTime := time.Now().Add(24 * 60 * time.Hour) 32 | 33 | claims := &MyClaims{ 34 | UserID: userID, 35 | UserEmail: userEmail, 36 | RegisteredClaims: jwt.RegisteredClaims{ 37 | ExpiresAt: jwt.NewNumericDate(expirationTime), 38 | }, 39 | } 40 | 41 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 42 | 43 | tokenString, err := token.SignedString(jwtKey) 44 | if err != nil { 45 | return "", err 46 | } 47 | 48 | return tokenString, nil 49 | } 50 | 51 | func VerifyJWT(tokenString string) (*MyClaims, error) { 52 | jwtKey := []byte("some-key") 53 | claims := &MyClaims{} 54 | 55 | token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { 56 | return jwtKey, nil 57 | }) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | if !token.Valid { 63 | return nil, fmt.Errorf("invalid token") 64 | } 65 | 66 | return claims, nil 67 | } 68 | 69 | func createCookie(exp time.Time, key string, value string) http.Cookie { 70 | return http.Cookie{ 71 | Name: key, 72 | Value: value, 73 | Expires: exp, 74 | HttpOnly: false, 75 | Path: "/", 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /views/components/Hero_templ.go: -------------------------------------------------------------------------------- 1 | // Code generated by templ - DO NOT EDIT. 2 | 3 | // templ: version: v0.2.747 4 | package components 5 | 6 | //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 | 8 | import "github.com/a-h/templ" 9 | import templruntime "github.com/a-h/templ/runtime" 10 | 11 | func Hero(heading string) templ.Component { 12 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 13 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 14 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 15 | if !templ_7745c5c3_IsBuffer { 16 | defer func() { 17 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 18 | if templ_7745c5c3_Err == nil { 19 | templ_7745c5c3_Err = templ_7745c5c3_BufErr 20 | } 21 | }() 22 | } 23 | ctx = templ.InitializeContext(ctx) 24 | templ_7745c5c3_Var1 := templ.GetChildren(ctx) 25 | if templ_7745c5c3_Var1 == nil { 26 | templ_7745c5c3_Var1 = templ.NopComponent 27 | } 28 | ctx = templ.ClearChildren(ctx) 29 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) 30 | if templ_7745c5c3_Err != nil { 31 | return templ_7745c5c3_Err 32 | } 33 | var templ_7745c5c3_Var2 string 34 | templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(heading) 35 | if templ_7745c5c3_Err != nil { 36 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/components/Hero.templ`, Line: 5, Col: 43} 37 | } 38 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 39 | if templ_7745c5c3_Err != nil { 40 | return templ_7745c5c3_Err 41 | } 42 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) 43 | if templ_7745c5c3_Err != nil { 44 | return templ_7745c5c3_Err 45 | } 46 | return templ_7745c5c3_Err 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /views/pages/index.templ: -------------------------------------------------------------------------------- 1 | package pages 2 | 3 | import ( 4 | "templtest/views/components" 5 | "templtest/views/layouts" 6 | "templtest/internal/entities" 7 | "fmt" 8 | ) 9 | 10 | templ Index(user entities.User, todos []entities.Todo){ 11 | @layouts.BaseLayout("Go app"){ 12 | @components.Header(user) 13 |
14 |
15 |
16 | @TodoList(todos) 17 |
18 | Пока нету Todos 19 |
20 |
21 | @TodoCreate() 22 |
23 |
24 | } 25 | } 26 | 27 | templ TodoComponent(todo entities.Todo){ 28 |
29 | {todo.Title} 30 | 32 |
33 | } 34 | 35 | templ TodoList(todos []entities.Todo){ 36 | if len(todos) > 0 { 37 | for _, todo := range todos{ 38 | @TodoComponent(todo) 39 | } 40 | } 41 | } 42 | 43 | templ TodoCreate(){ 44 |
45 | 46 | 47 | 48 |
49 | } -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 2 | github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= 3 | github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= 4 | github.com/a-h/templ v0.2.747 h1:D0dQ2lxC3W7Dxl6fxQ/1zZHBQslSkTSvl5FxP/CfdKg= 5 | github.com/a-h/templ v0.2.747/go.mod h1:69ObQIbrcuwPCU32ohNaWce3Cb7qM5GMiqN1K+2yop4= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 8 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 9 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 10 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 11 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 12 | github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= 13 | github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 14 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= 15 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= 16 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= 17 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= 18 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 19 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 20 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 21 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 22 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 23 | -------------------------------------------------------------------------------- /internal/storage/sqlite/todo.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "templtest/internal/entities" 5 | 6 | "github.com/Masterminds/squirrel" 7 | sq "github.com/Masterminds/squirrel" 8 | ) 9 | 10 | func (repo *Repository) CreateTodo(todo entities.Todo) error { 11 | // Создание запроса с использованием Squirrel 12 | query, args, err := sq.Insert("todos"). 13 | Columns("id", "title", "is_completed", "user_id"). 14 | Values(todo.ID, todo.Title, todo.IsCompleted, todo.UserID). 15 | ToSql() 16 | if err != nil { 17 | return err 18 | } 19 | 20 | // Выполнение запроса 21 | _, err = repo.db.Exec(query, args...) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | return nil 27 | } 28 | 29 | // Функция для получения всех задач пользователя с использованием Squirrel 30 | func (repo *Repository) GetTodos(userID string) ([]entities.Todo, error) { 31 | var todos []entities.Todo 32 | 33 | // Создание запроса с использованием Squirrel 34 | query, args, err := squirrel.Select("*"). 35 | From("todos"). 36 | Where(squirrel.Eq{"user_id": userID}). 37 | OrderBy("created_at DESC"). 38 | ToSql() 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | // Выполнение запроса и сканирование результата в срез Todo 44 | err = repo.db.Select(&todos, query, args...) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | return todos, nil 50 | } 51 | 52 | // Функция для обновления задачи (todo) с использованием Squirrel 53 | func (repo *Repository) UpdateTodo(todo entities.Todo) error { 54 | // Создание запроса с использованием Squirrel 55 | query, args, err := squirrel.Update("todos"). 56 | Set("title", todo.Title). 57 | Set("is_completed", todo.IsCompleted). 58 | Where(squirrel.Eq{"id": todo.ID}). 59 | ToSql() 60 | if err != nil { 61 | return err 62 | } 63 | 64 | // Выполнение запроса 65 | _, err = repo.db.Exec(query, args...) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (repo *Repository) DeleteTodo(ID string, UserID string) error { 74 | query, args, err := sq.Delete("todos"). 75 | Where(squirrel.Eq{"id": ID, "user_id": UserID}).ToSql() 76 | if err != nil { 77 | return err 78 | } 79 | _, err = repo.db.Exec(query, args...) 80 | if err != nil { 81 | return err 82 | } 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /views/layouts/base_templ.go: -------------------------------------------------------------------------------- 1 | // Code generated by templ - DO NOT EDIT. 2 | 3 | // templ: version: v0.2.747 4 | package layouts 5 | 6 | //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 | 8 | import "github.com/a-h/templ" 9 | import templruntime "github.com/a-h/templ/runtime" 10 | 11 | func BaseLayout(title string) templ.Component { 12 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 13 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 14 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 15 | if !templ_7745c5c3_IsBuffer { 16 | defer func() { 17 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 18 | if templ_7745c5c3_Err == nil { 19 | templ_7745c5c3_Err = templ_7745c5c3_BufErr 20 | } 21 | }() 22 | } 23 | ctx = templ.InitializeContext(ctx) 24 | templ_7745c5c3_Var1 := templ.GetChildren(ctx) 25 | if templ_7745c5c3_Var1 == nil { 26 | templ_7745c5c3_Var1 = templ.NopComponent 27 | } 28 | ctx = templ.ClearChildren(ctx) 29 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) 30 | if templ_7745c5c3_Err != nil { 31 | return templ_7745c5c3_Err 32 | } 33 | var templ_7745c5c3_Var2 string 34 | templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title) 35 | if templ_7745c5c3_Err != nil { 36 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/layouts/base.templ`, Line: 9, Col: 17} 37 | } 38 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) 39 | if templ_7745c5c3_Err != nil { 40 | return templ_7745c5c3_Err 41 | } 42 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) 43 | if templ_7745c5c3_Err != nil { 44 | return templ_7745c5c3_Err 45 | } 46 | templ_7745c5c3_Err = templ_7745c5c3_Var1.Render(ctx, templ_7745c5c3_Buffer) 47 | if templ_7745c5c3_Err != nil { 48 | return templ_7745c5c3_Err 49 | } 50 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 3) 51 | if templ_7745c5c3_Err != nil { 52 | return templ_7745c5c3_Err 53 | } 54 | return templ_7745c5c3_Err 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /internal/transport/web/handlers.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "templtest/internal/services" 8 | "templtest/internal/storage/sqlite" 9 | "templtest/views/pages" 10 | "time" 11 | ) 12 | 13 | type Handler struct { 14 | s *services.Service 15 | } 16 | 17 | func NewHandlers() *Handler { 18 | repo, err := sqlite.New() 19 | if err != nil { 20 | log.Fatal(err.Error()) 21 | } 22 | s := services.New(repo) 23 | return &Handler{s} 24 | } 25 | 26 | func (h *Handler) Index(w http.ResponseWriter, r *http.Request) { 27 | token, err := getAuthCookie(r) 28 | if err != nil { 29 | http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) 30 | return 31 | } 32 | claims, err := VerifyJWT(token) 33 | user, err := h.s.User(claims.UserEmail) 34 | if err != nil { 35 | fmt.Println(err.Error()) 36 | http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) 37 | return 38 | } 39 | todos, err := h.s.Todos(user.ID) 40 | if err != nil { 41 | fmt.Println(err.Error()) 42 | } 43 | pages.Index(user, todos).Render(r.Context(), w) 44 | } 45 | 46 | func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { 47 | email := r.FormValue("email") 48 | password := r.FormValue("pass") 49 | name := r.FormValue("name") 50 | fmt.Println(email, password) 51 | if email != "" && password != "" { 52 | user, err := h.s.UserLogin(email, name, password) 53 | if err == nil { 54 | if user.Password != password { 55 | pages.LoginPage("Wrong password").Render(r.Context(), w) 56 | return 57 | } 58 | token, _ := CreateJWT(user.ID, user.Email) 59 | cookie := createCookie(time.Now().Add(24*60*time.Hour), "Authorization", token) 60 | http.SetCookie(w, &cookie) 61 | http.Redirect(w, r, "/", http.StatusTemporaryRedirect) 62 | } 63 | } 64 | pages.LoginPage("").Render(r.Context(), w) 65 | } 66 | 67 | func (h *Handler) LoginForm(w http.ResponseWriter, r *http.Request) { 68 | w.Write([]byte("some")) 69 | } 70 | 71 | func (h *Handler) CreateTodo(w http.ResponseWriter, r *http.Request) { 72 | token, err := getAuthCookie(r) 73 | if err != nil { 74 | http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) 75 | return 76 | } 77 | title := r.FormValue("title") 78 | claims, err := VerifyJWT(token) 79 | todo, err := h.s.CreateTodos(title, claims.UserID) 80 | if err != nil { 81 | fmt.Println(err.Error()) 82 | } 83 | pages.TodoComponent(todo).Render(r.Context(), w) 84 | } 85 | 86 | func (h *Handler) DeleteTodo(w http.ResponseWriter, r *http.Request) { 87 | token, err := getAuthCookie(r) 88 | if err != nil { 89 | http.Redirect(w, r, "/login", http.StatusTemporaryRedirect) 90 | return 91 | } 92 | claims, err := VerifyJWT(token) 93 | id := r.PathValue("id") 94 | err = h.s.DeleteTodo(id, claims.UserID) 95 | 96 | w.WriteHeader(http.StatusAccepted) 97 | todos, err := h.s.Todos(claims.UserID) 98 | if err != nil { 99 | fmt.Println(err.Error()) 100 | } 101 | pages.TodoList(todos).Render(r.Context(), w) 102 | } 103 | -------------------------------------------------------------------------------- /views/pages/login_templ.go: -------------------------------------------------------------------------------- 1 | // Code generated by templ - DO NOT EDIT. 2 | 3 | // templ: version: v0.2.747 4 | package pages 5 | 6 | //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 | 8 | import "github.com/a-h/templ" 9 | import templruntime "github.com/a-h/templ/runtime" 10 | 11 | import "templtest/views/layouts" 12 | 13 | func LoginPage(err string) templ.Component { 14 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 15 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 16 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 17 | if !templ_7745c5c3_IsBuffer { 18 | defer func() { 19 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 20 | if templ_7745c5c3_Err == nil { 21 | templ_7745c5c3_Err = templ_7745c5c3_BufErr 22 | } 23 | }() 24 | } 25 | ctx = templ.InitializeContext(ctx) 26 | templ_7745c5c3_Var1 := templ.GetChildren(ctx) 27 | if templ_7745c5c3_Var1 == nil { 28 | templ_7745c5c3_Var1 = templ.NopComponent 29 | } 30 | ctx = templ.ClearChildren(ctx) 31 | templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 32 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 33 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 34 | if !templ_7745c5c3_IsBuffer { 35 | defer func() { 36 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 37 | if templ_7745c5c3_Err == nil { 38 | templ_7745c5c3_Err = templ_7745c5c3_BufErr 39 | } 40 | }() 41 | } 42 | ctx = templ.InitializeContext(ctx) 43 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) 44 | if templ_7745c5c3_Err != nil { 45 | return templ_7745c5c3_Err 46 | } 47 | if err != "" { 48 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) 49 | if templ_7745c5c3_Err != nil { 50 | return templ_7745c5c3_Err 51 | } 52 | var templ_7745c5c3_Var3 string 53 | templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(err) 54 | if templ_7745c5c3_Err != nil { 55 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages/login.templ`, Line: 11, Col: 83} 56 | } 57 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 58 | if templ_7745c5c3_Err != nil { 59 | return templ_7745c5c3_Err 60 | } 61 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 3) 62 | if templ_7745c5c3_Err != nil { 63 | return templ_7745c5c3_Err 64 | } 65 | } 66 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 4) 67 | if templ_7745c5c3_Err != nil { 68 | return templ_7745c5c3_Err 69 | } 70 | return templ_7745c5c3_Err 71 | }) 72 | templ_7745c5c3_Err = layouts.BaseLayout("Go app").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) 73 | if templ_7745c5c3_Err != nil { 74 | return templ_7745c5c3_Err 75 | } 76 | return templ_7745c5c3_Err 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /views/components/Header_templ.go: -------------------------------------------------------------------------------- 1 | // Code generated by templ - DO NOT EDIT. 2 | 3 | // templ: version: v0.2.747 4 | package components 5 | 6 | //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 | 8 | import "github.com/a-h/templ" 9 | import templruntime "github.com/a-h/templ/runtime" 10 | 11 | import ( 12 | "templtest/internal/entities" 13 | ) 14 | 15 | func Header(user entities.User) templ.Component { 16 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 17 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 18 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 19 | if !templ_7745c5c3_IsBuffer { 20 | defer func() { 21 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 22 | if templ_7745c5c3_Err == nil { 23 | templ_7745c5c3_Err = templ_7745c5c3_BufErr 24 | } 25 | }() 26 | } 27 | ctx = templ.InitializeContext(ctx) 28 | templ_7745c5c3_Var1 := templ.GetChildren(ctx) 29 | if templ_7745c5c3_Var1 == nil { 30 | templ_7745c5c3_Var1 = templ.NopComponent 31 | } 32 | ctx = templ.ClearChildren(ctx) 33 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) 34 | if templ_7745c5c3_Err != nil { 35 | return templ_7745c5c3_Err 36 | } 37 | templ_7745c5c3_Err = Profile(user.Name, user.Email).Render(ctx, templ_7745c5c3_Buffer) 38 | if templ_7745c5c3_Err != nil { 39 | return templ_7745c5c3_Err 40 | } 41 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) 42 | if templ_7745c5c3_Err != nil { 43 | return templ_7745c5c3_Err 44 | } 45 | return templ_7745c5c3_Err 46 | }) 47 | } 48 | 49 | func Profile(userName, userEmail string) templ.Component { 50 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 51 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 52 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 53 | if !templ_7745c5c3_IsBuffer { 54 | defer func() { 55 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 56 | if templ_7745c5c3_Err == nil { 57 | templ_7745c5c3_Err = templ_7745c5c3_BufErr 58 | } 59 | }() 60 | } 61 | ctx = templ.InitializeContext(ctx) 62 | templ_7745c5c3_Var2 := templ.GetChildren(ctx) 63 | if templ_7745c5c3_Var2 == nil { 64 | templ_7745c5c3_Var2 = templ.NopComponent 65 | } 66 | ctx = templ.ClearChildren(ctx) 67 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 3) 68 | if templ_7745c5c3_Err != nil { 69 | return templ_7745c5c3_Err 70 | } 71 | var templ_7745c5c3_Var3 string 72 | templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(userName) 73 | if templ_7745c5c3_Err != nil { 74 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/components/Header.templ`, Line: 18, Col: 51} 75 | } 76 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 77 | if templ_7745c5c3_Err != nil { 78 | return templ_7745c5c3_Err 79 | } 80 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 4) 81 | if templ_7745c5c3_Err != nil { 82 | return templ_7745c5c3_Err 83 | } 84 | var templ_7745c5c3_Var4 string 85 | templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(userEmail) 86 | if templ_7745c5c3_Err != nil { 87 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/components/Header.templ`, Line: 19, Col: 61} 88 | } 89 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 90 | if templ_7745c5c3_Err != nil { 91 | return templ_7745c5c3_Err 92 | } 93 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 5) 94 | if templ_7745c5c3_Err != nil { 95 | return templ_7745c5c3_Err 96 | } 97 | return templ_7745c5c3_Err 98 | }) 99 | } 100 | -------------------------------------------------------------------------------- /views/pages/index_templ.go: -------------------------------------------------------------------------------- 1 | // Code generated by templ - DO NOT EDIT. 2 | 3 | // templ: version: v0.2.747 4 | package pages 5 | 6 | //lint:file-ignore SA4006 This context is only used if a nested component is present. 7 | 8 | import "github.com/a-h/templ" 9 | import templruntime "github.com/a-h/templ/runtime" 10 | 11 | import ( 12 | "fmt" 13 | "templtest/internal/entities" 14 | "templtest/views/components" 15 | "templtest/views/layouts" 16 | ) 17 | 18 | func Index(user entities.User, todos []entities.Todo) templ.Component { 19 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 20 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 21 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 22 | if !templ_7745c5c3_IsBuffer { 23 | defer func() { 24 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 25 | if templ_7745c5c3_Err == nil { 26 | templ_7745c5c3_Err = templ_7745c5c3_BufErr 27 | } 28 | }() 29 | } 30 | ctx = templ.InitializeContext(ctx) 31 | templ_7745c5c3_Var1 := templ.GetChildren(ctx) 32 | if templ_7745c5c3_Var1 == nil { 33 | templ_7745c5c3_Var1 = templ.NopComponent 34 | } 35 | ctx = templ.ClearChildren(ctx) 36 | templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 37 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 38 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 39 | if !templ_7745c5c3_IsBuffer { 40 | defer func() { 41 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 42 | if templ_7745c5c3_Err == nil { 43 | templ_7745c5c3_Err = templ_7745c5c3_BufErr 44 | } 45 | }() 46 | } 47 | ctx = templ.InitializeContext(ctx) 48 | templ_7745c5c3_Err = components.Header(user).Render(ctx, templ_7745c5c3_Buffer) 49 | if templ_7745c5c3_Err != nil { 50 | return templ_7745c5c3_Err 51 | } 52 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 1) 53 | if templ_7745c5c3_Err != nil { 54 | return templ_7745c5c3_Err 55 | } 56 | var templ_7745c5c3_Var3 string 57 | templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("{ todosCount: %v }", len(todos))) 58 | if templ_7745c5c3_Err != nil { 59 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages/index.templ`, Line: 13, Col: 98} 60 | } 61 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 62 | if templ_7745c5c3_Err != nil { 63 | return templ_7745c5c3_Err 64 | } 65 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 2) 66 | if templ_7745c5c3_Err != nil { 67 | return templ_7745c5c3_Err 68 | } 69 | templ_7745c5c3_Err = TodoList(todos).Render(ctx, templ_7745c5c3_Buffer) 70 | if templ_7745c5c3_Err != nil { 71 | return templ_7745c5c3_Err 72 | } 73 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 3) 74 | if templ_7745c5c3_Err != nil { 75 | return templ_7745c5c3_Err 76 | } 77 | templ_7745c5c3_Err = TodoCreate().Render(ctx, templ_7745c5c3_Buffer) 78 | if templ_7745c5c3_Err != nil { 79 | return templ_7745c5c3_Err 80 | } 81 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 4) 82 | if templ_7745c5c3_Err != nil { 83 | return templ_7745c5c3_Err 84 | } 85 | return templ_7745c5c3_Err 86 | }) 87 | templ_7745c5c3_Err = layouts.BaseLayout("Go app").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) 88 | if templ_7745c5c3_Err != nil { 89 | return templ_7745c5c3_Err 90 | } 91 | return templ_7745c5c3_Err 92 | }) 93 | } 94 | 95 | func TodoComponent(todo entities.Todo) templ.Component { 96 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 97 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 98 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 99 | if !templ_7745c5c3_IsBuffer { 100 | defer func() { 101 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 102 | if templ_7745c5c3_Err == nil { 103 | templ_7745c5c3_Err = templ_7745c5c3_BufErr 104 | } 105 | }() 106 | } 107 | ctx = templ.InitializeContext(ctx) 108 | templ_7745c5c3_Var4 := templ.GetChildren(ctx) 109 | if templ_7745c5c3_Var4 == nil { 110 | templ_7745c5c3_Var4 = templ.NopComponent 111 | } 112 | ctx = templ.ClearChildren(ctx) 113 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 5) 114 | if templ_7745c5c3_Err != nil { 115 | return templ_7745c5c3_Err 116 | } 117 | var templ_7745c5c3_Var5 string 118 | templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(todo.Title) 119 | if templ_7745c5c3_Err != nil { 120 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages/index.templ`, Line: 29, Col: 61} 121 | } 122 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) 123 | if templ_7745c5c3_Err != nil { 124 | return templ_7745c5c3_Err 125 | } 126 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 6) 127 | if templ_7745c5c3_Err != nil { 128 | return templ_7745c5c3_Err 129 | } 130 | var templ_7745c5c3_Var6 string 131 | templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(todo.ID) 132 | if templ_7745c5c3_Err != nil { 133 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages/index.templ`, Line: 30, Col: 23} 134 | } 135 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) 136 | if templ_7745c5c3_Err != nil { 137 | return templ_7745c5c3_Err 138 | } 139 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 7) 140 | if templ_7745c5c3_Err != nil { 141 | return templ_7745c5c3_Err 142 | } 143 | var templ_7745c5c3_Var7 string 144 | templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs("/todos/" + todo.ID) 145 | if templ_7745c5c3_Err != nil { 146 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages/index.templ`, Line: 31, Col: 36} 147 | } 148 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) 149 | if templ_7745c5c3_Err != nil { 150 | return templ_7745c5c3_Err 151 | } 152 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 8) 153 | if templ_7745c5c3_Err != nil { 154 | return templ_7745c5c3_Err 155 | } 156 | return templ_7745c5c3_Err 157 | }) 158 | } 159 | 160 | func TodoList(todos []entities.Todo) templ.Component { 161 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 162 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 163 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 164 | if !templ_7745c5c3_IsBuffer { 165 | defer func() { 166 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 167 | if templ_7745c5c3_Err == nil { 168 | templ_7745c5c3_Err = templ_7745c5c3_BufErr 169 | } 170 | }() 171 | } 172 | ctx = templ.InitializeContext(ctx) 173 | templ_7745c5c3_Var8 := templ.GetChildren(ctx) 174 | if templ_7745c5c3_Var8 == nil { 175 | templ_7745c5c3_Var8 = templ.NopComponent 176 | } 177 | ctx = templ.ClearChildren(ctx) 178 | if len(todos) > 0 { 179 | for _, todo := range todos { 180 | templ_7745c5c3_Err = TodoComponent(todo).Render(ctx, templ_7745c5c3_Buffer) 181 | if templ_7745c5c3_Err != nil { 182 | return templ_7745c5c3_Err 183 | } 184 | } 185 | } 186 | return templ_7745c5c3_Err 187 | }) 188 | } 189 | 190 | func TodoCreate() templ.Component { 191 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 192 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 193 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 194 | if !templ_7745c5c3_IsBuffer { 195 | defer func() { 196 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 197 | if templ_7745c5c3_Err == nil { 198 | templ_7745c5c3_Err = templ_7745c5c3_BufErr 199 | } 200 | }() 201 | } 202 | ctx = templ.InitializeContext(ctx) 203 | templ_7745c5c3_Var9 := templ.GetChildren(ctx) 204 | if templ_7745c5c3_Var9 == nil { 205 | templ_7745c5c3_Var9 = templ.NopComponent 206 | } 207 | ctx = templ.ClearChildren(ctx) 208 | templ_7745c5c3_Err = templ.WriteWatchModeString(templ_7745c5c3_Buffer, 9) 209 | if templ_7745c5c3_Err != nil { 210 | return templ_7745c5c3_Err 211 | } 212 | return templ_7745c5c3_Err 213 | }) 214 | } 215 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.4.4 | 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 | 7. Disable tap highlights on iOS 36 | */ 37 | 38 | html, 39 | :host { 40 | line-height: 1.5; 41 | /* 1 */ 42 | -webkit-text-size-adjust: 100%; 43 | /* 2 */ 44 | -moz-tab-size: 4; 45 | /* 3 */ 46 | -o-tab-size: 4; 47 | tab-size: 4; 48 | /* 3 */ 49 | font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 50 | /* 4 */ 51 | font-feature-settings: normal; 52 | /* 5 */ 53 | font-variation-settings: normal; 54 | /* 6 */ 55 | -webkit-tap-highlight-color: transparent; 56 | /* 7 */ 57 | } 58 | 59 | /* 60 | 1. Remove the margin in all browsers. 61 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 62 | */ 63 | 64 | body { 65 | margin: 0; 66 | /* 1 */ 67 | line-height: inherit; 68 | /* 2 */ 69 | } 70 | 71 | /* 72 | 1. Add the correct height in Firefox. 73 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 74 | 3. Ensure horizontal rules are visible by default. 75 | */ 76 | 77 | hr { 78 | height: 0; 79 | /* 1 */ 80 | color: inherit; 81 | /* 2 */ 82 | border-top-width: 1px; 83 | /* 3 */ 84 | } 85 | 86 | /* 87 | Add the correct text decoration in Chrome, Edge, and Safari. 88 | */ 89 | 90 | abbr:where([title]) { 91 | -webkit-text-decoration: underline dotted; 92 | text-decoration: underline dotted; 93 | } 94 | 95 | /* 96 | Remove the default font size and weight for headings. 97 | */ 98 | 99 | h1, 100 | h2, 101 | h3, 102 | h4, 103 | h5, 104 | h6 { 105 | font-size: inherit; 106 | font-weight: inherit; 107 | } 108 | 109 | /* 110 | Reset links to optimize for opt-in styling instead of opt-out. 111 | */ 112 | 113 | a { 114 | color: inherit; 115 | text-decoration: inherit; 116 | } 117 | 118 | /* 119 | Add the correct font weight in Edge and Safari. 120 | */ 121 | 122 | b, 123 | strong { 124 | font-weight: bolder; 125 | } 126 | 127 | /* 128 | 1. Use the user's configured `mono` font-family by default. 129 | 2. Use the user's configured `mono` font-feature-settings by default. 130 | 3. Use the user's configured `mono` font-variation-settings by default. 131 | 4. Correct the odd `em` font sizing in all browsers. 132 | */ 133 | 134 | code, 135 | kbd, 136 | samp, 137 | pre { 138 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 139 | /* 1 */ 140 | font-feature-settings: normal; 141 | /* 2 */ 142 | font-variation-settings: normal; 143 | /* 3 */ 144 | font-size: 1em; 145 | /* 4 */ 146 | } 147 | 148 | /* 149 | Add the correct font size in all browsers. 150 | */ 151 | 152 | small { 153 | font-size: 80%; 154 | } 155 | 156 | /* 157 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 158 | */ 159 | 160 | sub, 161 | sup { 162 | font-size: 75%; 163 | line-height: 0; 164 | position: relative; 165 | vertical-align: baseline; 166 | } 167 | 168 | sub { 169 | bottom: -0.25em; 170 | } 171 | 172 | sup { 173 | top: -0.5em; 174 | } 175 | 176 | /* 177 | 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) 178 | 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) 179 | 3. Remove gaps between table borders by default. 180 | */ 181 | 182 | table { 183 | text-indent: 0; 184 | /* 1 */ 185 | border-color: inherit; 186 | /* 2 */ 187 | border-collapse: collapse; 188 | /* 3 */ 189 | } 190 | 191 | /* 192 | 1. Change the font styles in all browsers. 193 | 2. Remove the margin in Firefox and Safari. 194 | 3. Remove default padding in all browsers. 195 | */ 196 | 197 | button, 198 | input, 199 | optgroup, 200 | select, 201 | textarea { 202 | font-family: inherit; 203 | /* 1 */ 204 | font-feature-settings: inherit; 205 | /* 1 */ 206 | font-variation-settings: inherit; 207 | /* 1 */ 208 | font-size: 100%; 209 | /* 1 */ 210 | font-weight: inherit; 211 | /* 1 */ 212 | line-height: inherit; 213 | /* 1 */ 214 | letter-spacing: inherit; 215 | /* 1 */ 216 | color: inherit; 217 | /* 1 */ 218 | margin: 0; 219 | /* 2 */ 220 | padding: 0; 221 | /* 3 */ 222 | } 223 | 224 | /* 225 | Remove the inheritance of text transform in Edge and Firefox. 226 | */ 227 | 228 | button, 229 | select { 230 | text-transform: none; 231 | } 232 | 233 | /* 234 | 1. Correct the inability to style clickable types in iOS and Safari. 235 | 2. Remove default button styles. 236 | */ 237 | 238 | button, 239 | input:where([type='button']), 240 | input:where([type='reset']), 241 | input:where([type='submit']) { 242 | -webkit-appearance: button; 243 | /* 1 */ 244 | background-color: transparent; 245 | /* 2 */ 246 | background-image: none; 247 | /* 2 */ 248 | } 249 | 250 | /* 251 | Use the modern Firefox focus style for all focusable elements. 252 | */ 253 | 254 | :-moz-focusring { 255 | outline: auto; 256 | } 257 | 258 | /* 259 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 260 | */ 261 | 262 | :-moz-ui-invalid { 263 | box-shadow: none; 264 | } 265 | 266 | /* 267 | Add the correct vertical alignment in Chrome and Firefox. 268 | */ 269 | 270 | progress { 271 | vertical-align: baseline; 272 | } 273 | 274 | /* 275 | Correct the cursor style of increment and decrement buttons in Safari. 276 | */ 277 | 278 | ::-webkit-inner-spin-button, 279 | ::-webkit-outer-spin-button { 280 | height: auto; 281 | } 282 | 283 | /* 284 | 1. Correct the odd appearance in Chrome and Safari. 285 | 2. Correct the outline style in Safari. 286 | */ 287 | 288 | [type='search'] { 289 | -webkit-appearance: textfield; 290 | /* 1 */ 291 | outline-offset: -2px; 292 | /* 2 */ 293 | } 294 | 295 | /* 296 | Remove the inner padding in Chrome and Safari on macOS. 297 | */ 298 | 299 | ::-webkit-search-decoration { 300 | -webkit-appearance: none; 301 | } 302 | 303 | /* 304 | 1. Correct the inability to style clickable types in iOS and Safari. 305 | 2. Change font properties to `inherit` in Safari. 306 | */ 307 | 308 | ::-webkit-file-upload-button { 309 | -webkit-appearance: button; 310 | /* 1 */ 311 | font: inherit; 312 | /* 2 */ 313 | } 314 | 315 | /* 316 | Add the correct display in Chrome and Safari. 317 | */ 318 | 319 | summary { 320 | display: list-item; 321 | } 322 | 323 | /* 324 | Removes the default spacing and border for appropriate elements. 325 | */ 326 | 327 | blockquote, 328 | dl, 329 | dd, 330 | h1, 331 | h2, 332 | h3, 333 | h4, 334 | h5, 335 | h6, 336 | hr, 337 | figure, 338 | p, 339 | pre { 340 | margin: 0; 341 | } 342 | 343 | fieldset { 344 | margin: 0; 345 | padding: 0; 346 | } 347 | 348 | legend { 349 | padding: 0; 350 | } 351 | 352 | ol, 353 | ul, 354 | menu { 355 | list-style: none; 356 | margin: 0; 357 | padding: 0; 358 | } 359 | 360 | /* 361 | Reset default styling for dialogs. 362 | */ 363 | 364 | dialog { 365 | padding: 0; 366 | } 367 | 368 | /* 369 | Prevent resizing textareas horizontally by default. 370 | */ 371 | 372 | textarea { 373 | resize: vertical; 374 | } 375 | 376 | /* 377 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 378 | 2. Set the default placeholder color to the user's configured gray 400 color. 379 | */ 380 | 381 | input::-moz-placeholder, textarea::-moz-placeholder { 382 | opacity: 1; 383 | /* 1 */ 384 | color: #9ca3af; 385 | /* 2 */ 386 | } 387 | 388 | input::placeholder, 389 | textarea::placeholder { 390 | opacity: 1; 391 | /* 1 */ 392 | color: #9ca3af; 393 | /* 2 */ 394 | } 395 | 396 | /* 397 | Set the default cursor for buttons. 398 | */ 399 | 400 | button, 401 | [role="button"] { 402 | cursor: pointer; 403 | } 404 | 405 | /* 406 | Make sure disabled buttons don't get the pointer cursor. 407 | */ 408 | 409 | :disabled { 410 | cursor: default; 411 | } 412 | 413 | /* 414 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 415 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 416 | This can trigger a poorly considered lint error in some tools but is included by design. 417 | */ 418 | 419 | img, 420 | svg, 421 | video, 422 | canvas, 423 | audio, 424 | iframe, 425 | embed, 426 | object { 427 | display: block; 428 | /* 1 */ 429 | vertical-align: middle; 430 | /* 2 */ 431 | } 432 | 433 | /* 434 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 435 | */ 436 | 437 | img, 438 | video { 439 | max-width: 100%; 440 | height: auto; 441 | } 442 | 443 | /* Make elements with the HTML hidden attribute stay hidden by default */ 444 | 445 | [hidden] { 446 | display: none; 447 | } 448 | 449 | *, ::before, ::after { 450 | --tw-border-spacing-x: 0; 451 | --tw-border-spacing-y: 0; 452 | --tw-translate-x: 0; 453 | --tw-translate-y: 0; 454 | --tw-rotate: 0; 455 | --tw-skew-x: 0; 456 | --tw-skew-y: 0; 457 | --tw-scale-x: 1; 458 | --tw-scale-y: 1; 459 | --tw-pan-x: ; 460 | --tw-pan-y: ; 461 | --tw-pinch-zoom: ; 462 | --tw-scroll-snap-strictness: proximity; 463 | --tw-gradient-from-position: ; 464 | --tw-gradient-via-position: ; 465 | --tw-gradient-to-position: ; 466 | --tw-ordinal: ; 467 | --tw-slashed-zero: ; 468 | --tw-numeric-figure: ; 469 | --tw-numeric-spacing: ; 470 | --tw-numeric-fraction: ; 471 | --tw-ring-inset: ; 472 | --tw-ring-offset-width: 0px; 473 | --tw-ring-offset-color: #fff; 474 | --tw-ring-color: rgb(59 130 246 / 0.5); 475 | --tw-ring-offset-shadow: 0 0 #0000; 476 | --tw-ring-shadow: 0 0 #0000; 477 | --tw-shadow: 0 0 #0000; 478 | --tw-shadow-colored: 0 0 #0000; 479 | --tw-blur: ; 480 | --tw-brightness: ; 481 | --tw-contrast: ; 482 | --tw-grayscale: ; 483 | --tw-hue-rotate: ; 484 | --tw-invert: ; 485 | --tw-saturate: ; 486 | --tw-sepia: ; 487 | --tw-drop-shadow: ; 488 | --tw-backdrop-blur: ; 489 | --tw-backdrop-brightness: ; 490 | --tw-backdrop-contrast: ; 491 | --tw-backdrop-grayscale: ; 492 | --tw-backdrop-hue-rotate: ; 493 | --tw-backdrop-invert: ; 494 | --tw-backdrop-opacity: ; 495 | --tw-backdrop-saturate: ; 496 | --tw-backdrop-sepia: ; 497 | --tw-contain-size: ; 498 | --tw-contain-layout: ; 499 | --tw-contain-paint: ; 500 | --tw-contain-style: ; 501 | } 502 | 503 | ::backdrop { 504 | --tw-border-spacing-x: 0; 505 | --tw-border-spacing-y: 0; 506 | --tw-translate-x: 0; 507 | --tw-translate-y: 0; 508 | --tw-rotate: 0; 509 | --tw-skew-x: 0; 510 | --tw-skew-y: 0; 511 | --tw-scale-x: 1; 512 | --tw-scale-y: 1; 513 | --tw-pan-x: ; 514 | --tw-pan-y: ; 515 | --tw-pinch-zoom: ; 516 | --tw-scroll-snap-strictness: proximity; 517 | --tw-gradient-from-position: ; 518 | --tw-gradient-via-position: ; 519 | --tw-gradient-to-position: ; 520 | --tw-ordinal: ; 521 | --tw-slashed-zero: ; 522 | --tw-numeric-figure: ; 523 | --tw-numeric-spacing: ; 524 | --tw-numeric-fraction: ; 525 | --tw-ring-inset: ; 526 | --tw-ring-offset-width: 0px; 527 | --tw-ring-offset-color: #fff; 528 | --tw-ring-color: rgb(59 130 246 / 0.5); 529 | --tw-ring-offset-shadow: 0 0 #0000; 530 | --tw-ring-shadow: 0 0 #0000; 531 | --tw-shadow: 0 0 #0000; 532 | --tw-shadow-colored: 0 0 #0000; 533 | --tw-blur: ; 534 | --tw-brightness: ; 535 | --tw-contrast: ; 536 | --tw-grayscale: ; 537 | --tw-hue-rotate: ; 538 | --tw-invert: ; 539 | --tw-saturate: ; 540 | --tw-sepia: ; 541 | --tw-drop-shadow: ; 542 | --tw-backdrop-blur: ; 543 | --tw-backdrop-brightness: ; 544 | --tw-backdrop-contrast: ; 545 | --tw-backdrop-grayscale: ; 546 | --tw-backdrop-hue-rotate: ; 547 | --tw-backdrop-invert: ; 548 | --tw-backdrop-opacity: ; 549 | --tw-backdrop-saturate: ; 550 | --tw-backdrop-sepia: ; 551 | --tw-contain-size: ; 552 | --tw-contain-layout: ; 553 | --tw-contain-paint: ; 554 | --tw-contain-style: ; 555 | } 556 | 557 | .container { 558 | width: 100%; 559 | margin-right: auto; 560 | margin-left: auto; 561 | padding-right: 36px; 562 | padding-left: 36px; 563 | } 564 | 565 | @media (min-width: 1200px) { 566 | .container { 567 | max-width: 1200px; 568 | } 569 | } 570 | 571 | .visible { 572 | visibility: visible; 573 | } 574 | 575 | .mt-12 { 576 | margin-top: 3rem; 577 | } 578 | 579 | .flex { 580 | display: flex; 581 | } 582 | 583 | .h-96 { 584 | height: 24rem; 585 | } 586 | 587 | .h-dvh { 588 | height: 100dvh; 589 | } 590 | 591 | .w-64 { 592 | width: 16rem; 593 | } 594 | 595 | .w-72 { 596 | width: 18rem; 597 | } 598 | 599 | .w-full { 600 | width: 100%; 601 | } 602 | 603 | .flex-col { 604 | flex-direction: column; 605 | } 606 | 607 | .items-center { 608 | align-items: center; 609 | } 610 | 611 | .justify-center { 612 | justify-content: center; 613 | } 614 | 615 | .justify-between { 616 | justify-content: space-between; 617 | } 618 | 619 | .gap-1 { 620 | gap: 0.25rem; 621 | } 622 | 623 | .gap-2 { 624 | gap: 0.5rem; 625 | } 626 | 627 | .gap-3 { 628 | gap: 0.75rem; 629 | } 630 | 631 | .gap-6 { 632 | gap: 1.5rem; 633 | } 634 | 635 | .rounded { 636 | border-radius: 0.25rem; 637 | } 638 | 639 | .rounded-lg { 640 | border-radius: 0.5rem; 641 | } 642 | 643 | .rounded-xl { 644 | border-radius: 0.75rem; 645 | } 646 | 647 | .border { 648 | border-width: 1px; 649 | } 650 | 651 | .border-red-500 { 652 | --tw-border-opacity: 1; 653 | border-color: rgb(239 68 68 / var(--tw-border-opacity)); 654 | } 655 | 656 | .border-zinc-200 { 657 | --tw-border-opacity: 1; 658 | border-color: rgb(228 228 231 / var(--tw-border-opacity)); 659 | } 660 | 661 | .border-zinc-300 { 662 | --tw-border-opacity: 1; 663 | border-color: rgb(212 212 216 / var(--tw-border-opacity)); 664 | } 665 | 666 | .border-zinc-400 { 667 | --tw-border-opacity: 1; 668 | border-color: rgb(161 161 170 / var(--tw-border-opacity)); 669 | } 670 | 671 | .bg-blue-600 { 672 | --tw-bg-opacity: 1; 673 | background-color: rgb(37 99 235 / var(--tw-bg-opacity)); 674 | } 675 | 676 | .bg-blue-900 { 677 | --tw-bg-opacity: 1; 678 | background-color: rgb(30 58 138 / var(--tw-bg-opacity)); 679 | } 680 | 681 | .bg-gray-800 { 682 | --tw-bg-opacity: 1; 683 | background-color: rgb(31 41 55 / var(--tw-bg-opacity)); 684 | } 685 | 686 | .bg-red-100 { 687 | --tw-bg-opacity: 1; 688 | background-color: rgb(254 226 226 / var(--tw-bg-opacity)); 689 | } 690 | 691 | .bg-red-500 { 692 | --tw-bg-opacity: 1; 693 | background-color: rgb(239 68 68 / var(--tw-bg-opacity)); 694 | } 695 | 696 | .bg-zinc-50 { 697 | --tw-bg-opacity: 1; 698 | background-color: rgb(250 250 250 / var(--tw-bg-opacity)); 699 | } 700 | 701 | .p-2 { 702 | padding: 0.5rem; 703 | } 704 | 705 | .p-4 { 706 | padding: 1rem; 707 | } 708 | 709 | .p-6 { 710 | padding: 1.5rem; 711 | } 712 | 713 | .py-3 { 714 | padding-top: 0.75rem; 715 | padding-bottom: 0.75rem; 716 | } 717 | 718 | .py-4 { 719 | padding-top: 1rem; 720 | padding-bottom: 1rem; 721 | } 722 | 723 | .text-2xl { 724 | font-size: 1.5rem; 725 | line-height: 2rem; 726 | } 727 | 728 | .text-4xl { 729 | font-size: 2.25rem; 730 | line-height: 2.5rem; 731 | } 732 | 733 | .text-lg { 734 | font-size: 1.125rem; 735 | line-height: 1.75rem; 736 | } 737 | 738 | .text-xs { 739 | font-size: 0.75rem; 740 | line-height: 1rem; 741 | } 742 | 743 | .font-bold { 744 | font-weight: 700; 745 | } 746 | 747 | .font-light { 748 | font-weight: 300; 749 | } 750 | 751 | .text-red-800 { 752 | --tw-text-opacity: 1; 753 | color: rgb(153 27 27 / var(--tw-text-opacity)); 754 | } 755 | 756 | .text-white { 757 | --tw-text-opacity: 1; 758 | color: rgb(255 255 255 / var(--tw-text-opacity)); 759 | } 760 | 761 | .text-zinc-500 { 762 | --tw-text-opacity: 1; 763 | color: rgb(113 113 122 / var(--tw-text-opacity)); 764 | } 765 | 766 | .text-zinc-600 { 767 | --tw-text-opacity: 1; 768 | color: rgb(82 82 91 / var(--tw-text-opacity)); 769 | } 770 | 771 | .text-zinc-800 { 772 | --tw-text-opacity: 1; 773 | color: rgb(39 39 42 / var(--tw-text-opacity)); 774 | } 775 | 776 | @keyframes fade-in { 777 | from { 778 | opacity: 0; 779 | } 780 | } 781 | 782 | @keyframes fade-out { 783 | to { 784 | opacity: 0; 785 | } 786 | } 787 | 788 | @keyframes slide-from-right { 789 | from { 790 | transform: translateX(90px); 791 | } 792 | } 793 | 794 | @keyframes slide-to-left { 795 | to { 796 | transform: translateX(-90px); 797 | } 798 | } 799 | 800 | /* define animations for the old and new content */ 801 | 802 | ::view-transition-old(slide-it) { 803 | animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out, 804 | 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; 805 | } 806 | 807 | ::view-transition-new(slide-it) { 808 | animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 809 | 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; 810 | } 811 | 812 | /* tie the view transition to a given CSS class */ 813 | 814 | .sample-transition { 815 | view-transition-name: slide-it; 816 | } 817 | 818 | @media (min-width: 768px) { 819 | .md\:w-1\/2 { 820 | width: 50%; 821 | } 822 | 823 | .md\:flex-row { 824 | flex-direction: row; 825 | } 826 | } 827 | --------------------------------------------------------------------------------