├── .gitignore ├── postcss.config.js ├── Makefile ├── tailwind.config.js ├── config └── init.go ├── internal ├── transport │ └── web │ │ ├── admin.go │ │ ├── new.go │ │ └── utils.go ├── services │ ├── register.go │ └── sqlite-admin │ │ └── new.go ├── storage │ └── sqlite │ │ ├── users.go │ │ └── new.go └── entities │ └── main.go ├── package.json ├── public ├── js │ └── animation.js └── style.css ├── cmd └── main.go ├── views ├── pages │ ├── index.templ │ └── index_templ.go ├── layouts │ ├── base.templ │ └── base_templ.go └── css │ └── main.css ├── go.mod ├── .air.toml └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | tmp/ 3 | .env -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /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 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config/init.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/joho/godotenv" 8 | ) 9 | 10 | var ( 11 | MainSQLitePath string 12 | ) 13 | 14 | func Initial() { 15 | err := godotenv.Load() 16 | if err != nil { 17 | log.Fatal("Error loading .env file") 18 | } 19 | MainSQLitePath = os.Getenv("MAINSQLITEPATH") 20 | } 21 | -------------------------------------------------------------------------------- /internal/transport/web/admin.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "templtest/views/pages" 7 | ) 8 | 9 | func (s *Server) index(w http.ResponseWriter, r *http.Request) { 10 | users, err := s.service.AllUsers() 11 | if err != nil { 12 | log.Println(err.Error()) 13 | } 14 | pages.Index(users).Render(r.Context(), w) 15 | } 16 | -------------------------------------------------------------------------------- /internal/services/register.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import sqliteadmin "templtest/internal/services/sqlite-admin" 4 | 5 | type DB interface { 6 | sqliteadmin.DB 7 | } 8 | 9 | type RegisterService struct { 10 | *sqliteadmin.SQLiteAdminService 11 | } 12 | 13 | func New(db DB) *RegisterService { 14 | sqliteAdminS := sqliteadmin.New(db) 15 | return &RegisterService{SQLiteAdminService: sqliteAdminS} 16 | } 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/js/animation.js: -------------------------------------------------------------------------------- 1 | function OnLoadAnimation() { 2 | gsap.from("#create-todo", { opacity: 0, y: 100, duration: 0.5 }); 3 | } 4 | 5 | const buttons = document.querySelectorAll(".deleteanimated"); 6 | buttons.forEach((element, i) => { 7 | console.log("element " + i); 8 | element.addEventListener("click", () => { 9 | console.log(element.id); 10 | }); 11 | }); 12 | 13 | window.addEventListener("load", OnLoadAnimation); 14 | -------------------------------------------------------------------------------- /internal/services/sqlite-admin/new.go: -------------------------------------------------------------------------------- 1 | package sqliteadmin 2 | 3 | import "templtest/internal/entities" 4 | 5 | type DB interface { 6 | Users() ([]entities.User, error) 7 | } 8 | 9 | type SQLiteAdminService struct { 10 | db DB 11 | } 12 | 13 | func New(db DB) *SQLiteAdminService { 14 | return &SQLiteAdminService{db} 15 | } 16 | 17 | func (s *SQLiteAdminService) AllUsers() ([]entities.User, error) { 18 | return s.db.Users() 19 | } 20 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "templtest/config" 6 | "templtest/internal/services" 7 | "templtest/internal/storage/sqlite" 8 | "templtest/internal/transport/web" 9 | ) 10 | 11 | func main() { 12 | config.Initial() 13 | repo, err := sqlite.New() 14 | if err != nil { 15 | log.Fatal("db connection failed") 16 | } 17 | 18 | service := services.New(repo) 19 | 20 | server := web.New(service) 21 | server.Serve() 22 | } 23 | -------------------------------------------------------------------------------- /views/pages/index.templ: -------------------------------------------------------------------------------- 1 | package pages 2 | 3 | import ( 4 | "templtest/views/layouts" 5 | "templtest/internal/entities" 6 | ) 7 | 8 | templ Index(users []entities.User){ 9 | @layouts.BaseLayout("Admin | Syncword"){ 10 |
11 | for _, user := range users { 12 |
13 | {user.Name} 14 | {user.Email} 15 |
16 | } 17 |
18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /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/joho/godotenv v1.5.1 // indirect 12 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect 13 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect 14 | github.com/mattn/go-sqlite3 v1.14.22 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /internal/storage/sqlite/users.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "templtest/internal/entities" 5 | 6 | sq "github.com/Masterminds/squirrel" 7 | ) 8 | 9 | func (repo *Repository) Users() ([]entities.User, error) { 10 | sql, arg, err := sq.Select("id", "name", "full_name", "email", "avatar", "language", "created_at").From("users").ToSql() 11 | if err != nil { 12 | return []entities.User{}, err 13 | } 14 | result := make([]entities.User, 0) 15 | err = repo.db.Select(&result, sql, arg...) 16 | if err != nil { 17 | return []entities.User{}, err 18 | } 19 | return result, nil 20 | } 21 | -------------------------------------------------------------------------------- /internal/storage/sqlite/new.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "fmt" 5 | "templtest/config" 6 | 7 | "github.com/jmoiron/sqlx" 8 | _ "github.com/mattn/go-sqlite3" 9 | ) 10 | 11 | type Repository struct { 12 | db *sqlx.DB 13 | } 14 | 15 | func New() (*Repository, error) { 16 | db, err := sqlx.Open("sqlite3", config.MainSQLitePath) 17 | if err != nil { 18 | return &Repository{}, err 19 | } 20 | 21 | err = db.Ping() 22 | if err != nil { 23 | return &Repository{}, err 24 | } 25 | 26 | newDB := &Repository{ 27 | db: db, 28 | } 29 | fmt.Println("db connected") 30 | fmt.Println(config.MainSQLitePath) 31 | return newDB, nil 32 | } 33 | 34 | func (repo *Repository) Close() { 35 | repo.db.Close() 36 | } 37 | -------------------------------------------------------------------------------- /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 | 16 | 17 | {children...} 18 | 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /internal/entities/main.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import "time" 4 | 5 | type User struct { 6 | ID string `db:"id" json:"id"` 7 | Name string `db:"name" json:"name"` 8 | FullName string `db:"full_name" json:"full_name"` 9 | Email string `db:"email" json:"email"` 10 | Avatar string `db:"avatar" json:"avatar"` 11 | Language string `db:"language" json:"language"` 12 | CreatedAt time.Time `db:"created_at" json:"created_at"` 13 | } 14 | 15 | type WordBasic struct { 16 | ID string `db:"id" json:"id"` 17 | Title string `db:"title" json:"title"` 18 | Description string `db:"description" json:"description"` 19 | FromLanguage string `db:"from_language" json:"from_language"` 20 | ToLanguage string `db:"to_language" json:"to_language"` 21 | Type string `db:"type" json:"type"` 22 | } 23 | 24 | type Word struct { 25 | WordBasic 26 | CreatedAt time.Time `db:"created_at" json:"created_at"` 27 | UpdatedAt time.Time `db:"updated_at" json:"updated_at"` 28 | UserID string `db:"user_id" json:"user_id"` 29 | } 30 | -------------------------------------------------------------------------------- /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 | "templtest/internal/services" 11 | 12 | "time" 13 | ) 14 | 15 | type Server struct { 16 | mux *http.ServeMux 17 | service *services.RegisterService 18 | } 19 | 20 | func New(service *services.RegisterService) *Server { 21 | s := &Server{service: service} 22 | s.register() 23 | return s 24 | } 25 | 26 | func (s *Server) register() { 27 | handlers := http.NewServeMux() 28 | main := http.NewServeMux() 29 | fsHandler := http.FileServer(http.Dir("./public")) 30 | main.Handle("/public/", http.StripPrefix("/public/", fsHandler)) 31 | main.HandleFunc("/", s.index) 32 | handlers.Handle("/metrics/", http.StripPrefix("/metrics", main)) 33 | s.mux = handlers 34 | } 35 | 36 | func (s *Server) Serve() { 37 | ctx, cancel := context.WithCancel(context.Background()) 38 | defer cancel() 39 | 40 | // Signal to close app 41 | osSignal := make(chan os.Signal, 1) 42 | signal.Notify(osSignal, os.Interrupt, syscall.SIGTERM) 43 | 44 | server := &http.Server{ 45 | Addr: ":4000", 46 | Handler: s.mux, 47 | } 48 | 49 | go func() { 50 | log.Printf("Starting server on port 4000") 51 | if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 52 | log.Fatalf("Server failed: %v", err) 53 | } 54 | }() 55 | 56 | <-osSignal 57 | log.Println("Shutdown signal received. Shutting down...") 58 | 59 | // Timeout to close handlers 60 | shutdownCtx, shutdownCancel := context.WithTimeout(ctx, 30*time.Second) 61 | defer shutdownCancel() 62 | 63 | // Stop the server 64 | if err := server.Shutdown(shutdownCtx); err != nil { 65 | log.Fatalf("Server shutdown failed: %v", err) 66 | } 67 | 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 | -------------------------------------------------------------------------------- /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/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 15 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 16 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= 17 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= 18 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= 19 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= 20 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 21 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 22 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 24 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 25 | -------------------------------------------------------------------------------- /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_7745c5c3_Buffer.WriteString("") 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_7745c5c3_Buffer.WriteString("") 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_7745c5c3_Buffer.WriteString("") 51 | if templ_7745c5c3_Err != nil { 52 | return templ_7745c5c3_Err 53 | } 54 | return templ_7745c5c3_Err 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /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 | "templtest/internal/entities" 13 | "templtest/views/layouts" 14 | ) 15 | 16 | func Index(users []entities.User) templ.Component { 17 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 18 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 19 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 20 | if !templ_7745c5c3_IsBuffer { 21 | defer func() { 22 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 23 | if templ_7745c5c3_Err == nil { 24 | templ_7745c5c3_Err = templ_7745c5c3_BufErr 25 | } 26 | }() 27 | } 28 | ctx = templ.InitializeContext(ctx) 29 | templ_7745c5c3_Var1 := templ.GetChildren(ctx) 30 | if templ_7745c5c3_Var1 == nil { 31 | templ_7745c5c3_Var1 = templ.NopComponent 32 | } 33 | ctx = templ.ClearChildren(ctx) 34 | templ_7745c5c3_Var2 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { 35 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context 36 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) 37 | if !templ_7745c5c3_IsBuffer { 38 | defer func() { 39 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) 40 | if templ_7745c5c3_Err == nil { 41 | templ_7745c5c3_Err = templ_7745c5c3_BufErr 42 | } 43 | }() 44 | } 45 | ctx = templ.InitializeContext(ctx) 46 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") 47 | if templ_7745c5c3_Err != nil { 48 | return templ_7745c5c3_Err 49 | } 50 | for _, user := range users { 51 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") 52 | if templ_7745c5c3_Err != nil { 53 | return templ_7745c5c3_Err 54 | } 55 | var templ_7745c5c3_Var3 string 56 | templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Name) 57 | if templ_7745c5c3_Err != nil { 58 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages/index.templ`, Line: 13, Col: 26} 59 | } 60 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) 61 | if templ_7745c5c3_Err != nil { 62 | return templ_7745c5c3_Err 63 | } 64 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(" ") 65 | if templ_7745c5c3_Err != nil { 66 | return templ_7745c5c3_Err 67 | } 68 | var templ_7745c5c3_Var4 string 69 | templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email) 70 | if templ_7745c5c3_Err != nil { 71 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages/index.templ`, Line: 14, Col: 27} 72 | } 73 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) 74 | if templ_7745c5c3_Err != nil { 75 | return templ_7745c5c3_Err 76 | } 77 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") 78 | if templ_7745c5c3_Err != nil { 79 | return templ_7745c5c3_Err 80 | } 81 | } 82 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString("
") 83 | if templ_7745c5c3_Err != nil { 84 | return templ_7745c5c3_Err 85 | } 86 | return templ_7745c5c3_Err 87 | }) 88 | templ_7745c5c3_Err = layouts.BaseLayout("Admin | Syncword").Render(templ.WithChildren(ctx, templ_7745c5c3_Var2), templ_7745c5c3_Buffer) 89 | if templ_7745c5c3_Err != nil { 90 | return templ_7745c5c3_Err 91 | } 92 | return templ_7745c5c3_Err 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /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 | .flex { 558 | display: flex; 559 | } 560 | 561 | .w-full { 562 | width: 100%; 563 | } 564 | 565 | .flex-col { 566 | flex-direction: column; 567 | } 568 | 569 | .justify-center { 570 | justify-content: center; 571 | } 572 | 573 | .gap-2 { 574 | gap: 0.5rem; 575 | } 576 | 577 | .py-16 { 578 | padding-top: 4rem; 579 | padding-bottom: 4rem; 580 | } 581 | 582 | @keyframes fade-in { 583 | from { 584 | opacity: 0; 585 | } 586 | } 587 | 588 | @keyframes fade-out { 589 | to { 590 | opacity: 0; 591 | } 592 | } 593 | 594 | @keyframes slide-from-right { 595 | from { 596 | transform: translateX(90px); 597 | } 598 | } 599 | 600 | @keyframes slide-to-left { 601 | to { 602 | transform: translateX(-90px); 603 | } 604 | } 605 | 606 | /* define animations for the old and new content */ 607 | 608 | ::view-transition-old(slide-it) { 609 | animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out, 610 | 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; 611 | } 612 | 613 | ::view-transition-new(slide-it) { 614 | animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 615 | 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; 616 | } 617 | 618 | /* tie the view transition to a given CSS class */ 619 | 620 | .sample-transition { 621 | view-transition-name: slide-it; 622 | } 623 | --------------------------------------------------------------------------------