diff options
author | Steven Le Rouzic <steven.lerouzic@gmail.com> | 2024-04-27 23:55:31 +0200 |
---|---|---|
committer | Steven Le Rouzic <steven.lerouzic@gmail.com> | 2024-04-27 23:55:31 +0200 |
commit | a55cfe8205d5cb2fb0948c173f23ad71d6614d13 (patch) | |
tree | 386a341b3bcc543ca8ac7bfe7ff944cac669fe25 | |
parent | df3068728abacfc98fa19f3dba62b35f65aea731 (diff) |
Rename everything to locker/lock
-rw-r--r-- | build.bat | 4 | ||||
-rw-r--r-- | css/input.css | 3 | ||||
-rw-r--r-- | database.go | 6 | ||||
-rw-r--r-- | go.mod | 2 | ||||
-rw-r--r-- | lock.db (renamed from timer.db) | bin | 28672 -> 28672 bytes | |||
-rw-r--r-- | locker.go (renamed from timer.go) | 136 | ||||
-rw-r--r-- | model/lock.go | 110 | ||||
-rw-r--r-- | model/timer.go | 110 | ||||
-rw-r--r-- | session.go | 6 | ||||
-rw-r--r-- | static/style.css | 10 | ||||
-rw-r--r-- | tailwind.config.js | 8 | ||||
-rw-r--r-- | view/lock.templ | 80 | ||||
-rw-r--r-- | view/locks_list.templ | 51 | ||||
-rw-r--r-- | view/login.templ | 2 | ||||
-rw-r--r-- | view/main.templ | 4 | ||||
-rw-r--r-- | view/timer.templ | 80 | ||||
-rw-r--r-- | view/timers_list.templ | 51 |
17 files changed, 335 insertions, 328 deletions
diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..5f7da18 --- /dev/null +++ b/build.bat @@ -0,0 +1,4 @@ +templ generate
+tailwindcss -i css\input.css -o static\style.css --minify
+
+
diff --git a/css/input.css b/css/input.css new file mode 100644 index 0000000..04b35af --- /dev/null +++ b/css/input.css @@ -0,0 +1,3 @@ +@tailwind base;
+@tailwind components;
+@tailwind utilities;
diff --git a/database.go b/database.go index c53b828..425f98a 100644 --- a/database.go +++ b/database.go @@ -6,7 +6,7 @@ import ( "golang.org/x/crypto/bcrypt" - "stevenlr.com/timer/model" + "stevenlr.com/locker/model" ) func initializeDatabaseV1(db *sql.DB) error { @@ -22,7 +22,7 @@ func initializeDatabaseV1(db *sql.DB) error { } _, err = tx.Exec(` - CREATE TABLE Timer ( + CREATE TABLE Lock ( Id BLOB NOT NULL UNIQUE, Name TEXT NOT NULL, StartTime TEXT NOT NULL, @@ -74,7 +74,7 @@ func migrateDatabaseV2(db *sql.DB) error { return err } - _, err = tx.Exec("CREATE INDEX TimerTokenIndex ON Timer(Token)") + _, err = tx.Exec("CREATE INDEX LockTokenIndex ON Lock(Token)") if err != nil { return err } @@ -1,4 +1,4 @@ -module stevenlr.com/timer +module stevenlr.com/locker go 1.22.2 @@ -12,39 +12,39 @@ import ( _ "github.com/mattn/go-sqlite3" - "stevenlr.com/timer/model" - "stevenlr.com/timer/utils" - "stevenlr.com/timer/view" + "stevenlr.com/locker/model" + "stevenlr.com/locker/utils" + "stevenlr.com/locker/view" ) -type TimerServer struct { +type LockServer struct { db *sql.DB sessions Sessions } -func (server *TimerServer) findCurrentUser(w http.ResponseWriter, r *http.Request) *model.User { +func (server *LockServer) findCurrentUser(w http.ResponseWriter, r *http.Request) *model.User { return server.sessions.FindCurrentUser(server.db, w, r) } -func (server *TimerServer) handleNotFound(w http.ResponseWriter, _ *http.Request) { +func (server *LockServer) handleNotFound(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNotFound) view.Error404().Render(context.Background(), w) } -func (server *TimerServer) handleMain(w http.ResponseWriter, r *http.Request) { +func (server *LockServer) handleMain(w http.ResponseWriter, r *http.Request) { currentUser := server.findCurrentUser(w, r) if r.URL.Path == "/" { - timers := make([]model.Timer, 0) + locks := make([]model.Lock, 0) if currentUser != nil { - timers = model.GetTimersForUser(server.db, currentUser.Id) + locks = model.GetLocksForUser(server.db, currentUser.Id) } - view.Main(view.TimersList(timers, currentUser != nil), currentUser).Render(context.Background(), w) + view.Main(view.LocksList(locks, currentUser != nil), currentUser).Render(context.Background(), w) } else { server.handleNotFound(w, r) } } -func (server *TimerServer) handleTimer(w http.ResponseWriter, r *http.Request) { +func (server *LockServer) handleLock(w http.ResponseWriter, r *http.Request) { currentUser := server.findCurrentUser(w, r) if currentUser == nil { server.handleNotFound(w, r) @@ -52,23 +52,23 @@ func (server *TimerServer) handleTimer(w http.ResponseWriter, r *http.Request) { } var id model.UUID - if err := id.Scan(r.PathValue("timerId")); err != nil { + if err := id.Scan(r.PathValue("lockId")); err != nil { server.handleNotFound(w, r) return } - timer := model.GetTimerForUser(server.db, id, currentUser.Id) - if timer != nil && timer.Owner == currentUser.Id { - view.Main(view.TimerView(*timer), currentUser).Render(context.Background(), w) + lock := model.GetLockForUser(server.db, id, currentUser.Id) + if lock != nil && lock.Owner == currentUser.Id { + view.Main(view.LockView(*lock), currentUser).Render(context.Background(), w) } else { server.handleNotFound(w, r) } } -func (server *TimerServer) handleTimerAddTimeCommon(w http.ResponseWriter, r *http.Request, timer *model.Timer) bool { - if timer.IsFinished() { +func (server *LockServer) handleLockAddTimeCommon(w http.ResponseWriter, r *http.Request, lock *model.Lock) bool { + if lock.IsFinished() { w.WriteHeader(http.StatusBadRequest) - w.Write([]byte("Timer already finished")) + w.Write([]byte("Lock already finished")) return false } @@ -79,8 +79,8 @@ func (server *TimerServer) handleTimerAddTimeCommon(w http.ResponseWriter, r *ht return false } - timer.EndTime.Add(duration) - res := model.UpdateTimerEndTime(server.db, timer.Id, timer.EndTime, timer.Owner) + lock.EndTime.Add(duration) + res := model.UpdateLockEndTime(server.db, lock.Id, lock.EndTime, lock.Owner) if !res { w.WriteHeader(http.StatusInternalServerError) return false @@ -89,7 +89,7 @@ func (server *TimerServer) handleTimerAddTimeCommon(w http.ResponseWriter, r *ht return true } -func (server *TimerServer) handleTimerAddTime(w http.ResponseWriter, r *http.Request) { +func (server *LockServer) handleLockAddTime(w http.ResponseWriter, r *http.Request) { currentUser := server.findCurrentUser(w, r) if currentUser == nil { w.WriteHeader(http.StatusUnauthorized) @@ -97,43 +97,43 @@ func (server *TimerServer) handleTimerAddTime(w http.ResponseWriter, r *http.Req } var id model.UUID - if err := id.Scan(r.PathValue("timerId")); err != nil { + if err := id.Scan(r.PathValue("lockId")); err != nil { w.WriteHeader(http.StatusNotFound) return } - timer := model.GetTimerForUser(server.db, id, currentUser.Id) - if timer == nil { + lock := model.GetLockForUser(server.db, id, currentUser.Id) + if lock == nil { w.WriteHeader(http.StatusNotFound) return } - if !server.handleTimerAddTimeCommon(w, r, timer) { + if !server.handleLockAddTimeCommon(w, r, lock) { return } - view.TimerInfo(*timer).Render(context.Background(), w) + view.LockInfo(*lock).Render(context.Background(), w) } -func (server *TimerServer) handleApiTimerAddTime(w http.ResponseWriter, r *http.Request) { +func (server *LockServer) handleApiLockAddTime(w http.ResponseWriter, r *http.Request) { var id model.UUID - if err := id.Scan(r.PathValue("timerId")); err != nil { + if err := id.Scan(r.PathValue("lockId")); err != nil { w.WriteHeader(http.StatusNotFound) return } - timer := model.GetTimerWithToken(server.db, id, r.FormValue("token")) - if timer == nil { + lock := model.GetLockWithToken(server.db, id, r.FormValue("token")) + if lock == nil { w.WriteHeader(http.StatusNotFound) return } - if !server.handleTimerAddTimeCommon(w, r, timer) { + if !server.handleLockAddTimeCommon(w, r, lock) { return } } -func (server *TimerServer) handleGetTimerToken(w http.ResponseWriter, r *http.Request) { +func (server *LockServer) handleGetLockToken(w http.ResponseWriter, r *http.Request) { currentUser := server.findCurrentUser(w, r) if currentUser == nil { w.WriteHeader(http.StatusUnauthorized) @@ -141,21 +141,21 @@ func (server *TimerServer) handleGetTimerToken(w http.ResponseWriter, r *http.Re } var id model.UUID - if err := id.Scan(r.PathValue("timerId")); err != nil { + if err := id.Scan(r.PathValue("lockId")); err != nil { w.WriteHeader(http.StatusNotFound) return } - timer := model.GetTimerForUser(server.db, id, currentUser.Id) - if timer == nil { + lock := model.GetLockForUser(server.db, id, currentUser.Id) + if lock == nil { server.handleNotFound(w, r) return } - w.Write([]byte(fmt.Sprint("<code>", timer.Token, "</code>"))) + w.Write([]byte(fmt.Sprint("<code>", lock.Token, "</code>"))) } -func (server *TimerServer) handleResetTimerToken(w http.ResponseWriter, r *http.Request) { +func (server *LockServer) handleResetLockToken(w http.ResponseWriter, r *http.Request) { currentUser := server.findCurrentUser(w, r) if currentUser == nil { w.WriteHeader(http.StatusUnauthorized) @@ -163,27 +163,27 @@ func (server *TimerServer) handleResetTimerToken(w http.ResponseWriter, r *http. } var id model.UUID - if err := id.Scan(r.PathValue("timerId")); err != nil { + if err := id.Scan(r.PathValue("lockId")); err != nil { w.WriteHeader(http.StatusNotFound) return } - timer := model.GetTimerForUser(server.db, id, currentUser.Id) - if timer == nil { + lock := model.GetLockForUser(server.db, id, currentUser.Id) + if lock == nil { w.WriteHeader(http.StatusNotFound) return } - res := model.RegenerateTimerToken(server.db, timer.Id, currentUser.Id) + res := model.RegenerateLockToken(server.db, lock.Id, currentUser.Id) if !res { w.WriteHeader(http.StatusInternalServerError) return } - view.TimerTokenForm(*timer).Render(context.Background(), w) + view.LockTokenForm(*lock).Render(context.Background(), w) } -func (server *TimerServer) handleDeleteTimer(w http.ResponseWriter, r *http.Request) { +func (server *LockServer) handleDeleteLock(w http.ResponseWriter, r *http.Request) { user := server.findCurrentUser(w, r) if user == nil { w.WriteHeader(http.StatusUnauthorized) @@ -191,69 +191,69 @@ func (server *TimerServer) handleDeleteTimer(w http.ResponseWriter, r *http.Requ } var id model.UUID - if err := id.Scan(r.PathValue("timerId")); err != nil { + if err := id.Scan(r.PathValue("lockId")); err != nil { w.WriteHeader(http.StatusNotFound) return } - success := model.DeleteTimer(server.db, id, user.Id) + success := model.DeleteLock(server.db, id, user.Id) if !success { w.WriteHeader(http.StatusNotFound) } } -func (server *TimerServer) handleCreateTimer(w http.ResponseWriter, r *http.Request) { - timerName := strings.TrimSpace(r.FormValue("timerName")) +func (server *LockServer) handleCreateLock(w http.ResponseWriter, r *http.Request) { + lockName := strings.TrimSpace(r.FormValue("lockName")) user := server.findCurrentUser(w, r) if user == nil { w.WriteHeader(http.StatusBadRequest) - view.TimerCreateForm(timerName, "You are not signed in").Render(context.Background(), w) + view.LockCreateForm(lockName, "You are not signed in").Render(context.Background(), w) return } days, err := utils.ParseNumber(r.FormValue("days")) if err != nil { w.WriteHeader(http.StatusBadRequest) - view.TimerCreateForm(timerName, "Error parsing days").Render(context.Background(), w) + view.LockCreateForm(lockName, "Error parsing days").Render(context.Background(), w) return } hours, err := utils.ParseNumber(r.FormValue("hours")) if err != nil { w.WriteHeader(http.StatusBadRequest) - view.TimerCreateForm(timerName, "Error parsing hours").Render(context.Background(), w) + view.LockCreateForm(lockName, "Error parsing hours").Render(context.Background(), w) return } tx, err := server.db.Begin() if err != nil { w.WriteHeader(http.StatusInternalServerError) - view.TimerCreateForm(timerName, "Internal server error").Render(context.Background(), w) + view.LockCreateForm(lockName, "Internal server error").Render(context.Background(), w) return } defer tx.Rollback() - if timerName == "" { + if lockName == "" { w.WriteHeader(http.StatusBadRequest) - view.TimerCreateForm("", "Timer name cannot be empty").Render(context.Background(), w) + view.LockCreateForm("", "Lock name cannot be empty").Render(context.Background(), w) return } - err = model.InsertTimer(tx, timerName, int(((max(days, 0)*24)+max(hours, 0))*3600), user.Id) + err = model.InsertLock(tx, lockName, int(((max(days, 0)*24)+max(hours, 0))*3600), user.Id) if err != nil { w.WriteHeader(http.StatusInternalServerError) - view.TimerCreateForm(timerName, "Internal server error").Render(context.Background(), w) + view.LockCreateForm(lockName, "Internal server error").Render(context.Background(), w) return } tx.Commit() - timers := model.GetTimersForUser(server.db, user.Id) - view.TimersList(timers, user != nil).Render(context.Background(), w) + locks := model.GetLocksForUser(server.db, user.Id) + view.LocksList(locks, user != nil).Render(context.Background(), w) } -func (server *TimerServer) handlePostLogin(w http.ResponseWriter, r *http.Request) { +func (server *LockServer) handlePostLogin(w http.ResponseWriter, r *http.Request) { if server.findCurrentUser(w, r) != nil { utils.HtmxRedirect(w, "/") return @@ -284,7 +284,7 @@ func (server *TimerServer) handlePostLogin(w http.ResponseWriter, r *http.Reques } } -func (server *TimerServer) handlePostLogout(w http.ResponseWriter, r *http.Request) { +func (server *LockServer) handlePostLogout(w http.ResponseWriter, r *http.Request) { server.sessions.EndSession(w, r) utils.HtmxRedirect(w, "/") } @@ -292,7 +292,7 @@ func (server *TimerServer) handlePostLogout(w http.ResponseWriter, r *http.Reque func main() { log.Println("Starting...") - db, err := sql.Open("sqlite3", "file:timer.db") + db, err := sql.Open("sqlite3", "file:lock.db") if err != nil { log.Fatalln(err) } @@ -302,20 +302,20 @@ func main() { log.Fatalln(err) } - myServer := TimerServer{db: db, sessions: MakeSessions()} + myServer := LockServer{db: db, sessions: MakeSessions()} fs := http.FileServer(http.Dir("static/")) http.Handle("GET /static/", http.StripPrefix("/static/", fs)) http.HandleFunc("POST /login", myServer.handlePostLogin) http.HandleFunc("POST /logout", myServer.handlePostLogout) - http.HandleFunc("GET /timer/{timerId}", myServer.handleTimer) - http.HandleFunc("POST /timer/{timerId}/addTime", myServer.handleTimerAddTime) - http.HandleFunc("POST /api/timer/{timerId}/addTime", myServer.handleApiTimerAddTime) - http.HandleFunc("DELETE /timer/{timerId}", myServer.handleDeleteTimer) - http.HandleFunc("POST /timer/{timerId}/resetToken", myServer.handleResetTimerToken) - http.HandleFunc("GET /timer/{timerId}/token", myServer.handleGetTimerToken) - http.HandleFunc("PUT /timer", myServer.handleCreateTimer) + http.HandleFunc("GET /lock/{lockId}", myServer.handleLock) + http.HandleFunc("POST /lock/{lockId}/addTime", myServer.handleLockAddTime) + http.HandleFunc("POST /api/lock/{lockId}/addTime", myServer.handleApiLockAddTime) + http.HandleFunc("DELETE /lock/{lockId}", myServer.handleDeleteLock) + http.HandleFunc("POST /lock/{lockId}/resetToken", myServer.handleResetLockToken) + http.HandleFunc("GET /lock/{lockId}/token", myServer.handleGetLockToken) + http.HandleFunc("PUT /lock", myServer.handleCreateLock) http.HandleFunc("GET /", myServer.handleMain) log.Println("Started!") diff --git a/model/lock.go b/model/lock.go new file mode 100644 index 0000000..555d31e --- /dev/null +++ b/model/lock.go @@ -0,0 +1,110 @@ +package model + +import ( + "database/sql" + "log" + "time" + + "stevenlr.com/locker/utils" +) + +func GenerateLockToken() (string, error) { + return utils.GenerateRandomString(66) +} + +type Lock struct { + Id UUID + Name string + StartTime Time + EndTime Time + Owner UUID + Token string +} + +func (self Lock) IsFinished() bool { + return MakeTimeNow().Compare(self.EndTime) >= 0 +} + +func InsertLock(tx *sql.Tx, name string, seconds int, ownerId UUID) error { + now := MakeTimeNow() + end := Time(time.Time(now).Add(time.Duration(seconds) * time.Second)) + id := MakeUUID() + token, _ := GenerateLockToken() + _, err := tx.Exec(` + INSERT INTO Lock VALUES ($1, $2, $3, $4, $5, $6)`, id, name, now, end, ownerId, token) + return err +} + +func GetLocksForUser(db *sql.DB, owner UUID) []Lock { + rows, err := db.Query("SELECT Id, Name FROM Lock WHERE Owner=$1", owner) + if err != nil { + log.Fatalln(err) + } + + locks := []Lock{} + for rows.Next() { + var t Lock + if err := rows.Scan(&t.Id, &t.Name); err == nil { + locks = append(locks, t) + } + } + + return locks +} + +func GetLockForUser(db *sql.DB, id UUID, userId UUID) *Lock { + row := db.QueryRow("SELECT Id, Name, StartTime, EndTime, Owner, Token FROM Lock WHERE Id=$1 AND Owner=$2", id, userId) + + var t Lock + if err := row.Scan(&t.Id, &t.Name, &t.StartTime, &t.EndTime, &t.Owner, &t.Token); err == nil { + return &t + } + + return nil +} + +func GetLockWithToken(db *sql.DB, id UUID, token string) *Lock { + row := db.QueryRow("SELECT Id, Name, StartTime, EndTime, Owner, Token FROM Lock WHERE Id=$1 AND Token=$2", id, token) + + var t Lock + if err := row.Scan(&t.Id, &t.Name, &t.StartTime, &t.EndTime, &t.Owner, &t.Token); err == nil { + return &t + } + + return nil +} + +func DeleteLock(db *sql.DB, id UUID, userId UUID) bool { + res, err := db.Exec("DELETE FROM Lock WHERE Id=$1 AND Owner=$2", id, userId) + if err != nil { + return false + } + + affected, err := res.RowsAffected() + return err == nil && affected == 1 +} + +func UpdateLockEndTime(db *sql.DB, id UUID, endTime Time, userId UUID) bool { + res, err := db.Exec("UPDATE Lock SET EndTime=$1 WHERE Id=$2 AND Owner=$3", endTime, id, userId) + if err != nil { + return false + } + + affected, err := res.RowsAffected() + return err == nil && affected == 1 +} + +func RegenerateLockToken(db *sql.DB, id UUID, userId UUID) bool { + newToken, err := GenerateLockToken() + if err != nil { + return false + } + + res, err := db.Exec("UPDATE Lock SET Token=$1 WHERE Id=$2 AND Owner=$3", newToken, id, userId) + if err != nil { + return false + } + + affected, err := res.RowsAffected() + return err == nil && affected == 1 +} diff --git a/model/timer.go b/model/timer.go deleted file mode 100644 index 3f13d0d..0000000 --- a/model/timer.go +++ /dev/null @@ -1,110 +0,0 @@ -package model - -import ( - "database/sql" - "log" - "time" - - "stevenlr.com/timer/utils" -) - -func GenerateTimerToken() (string, error) { - return utils.GenerateRandomString(66) -} - -type Timer struct { - Id UUID - Name string - StartTime Time - EndTime Time - Owner UUID - Token string -} - -func (self Timer) IsFinished() bool { - return MakeTimeNow().Compare(self.EndTime) >= 0 -} - -func InsertTimer(tx *sql.Tx, name string, seconds int, ownerId UUID) error { - now := MakeTimeNow() - end := Time(time.Time(now).Add(time.Duration(seconds) * time.Second)) - id := MakeUUID() - token, _ := GenerateTimerToken() - _, err := tx.Exec(` - INSERT INTO Timer VALUES ($1, $2, $3, $4, $5, $6)`, id, name, now, end, ownerId, token) - return err -} - -func GetTimersForUser(db *sql.DB, owner UUID) []Timer { - rows, err := db.Query("SELECT Id, Name FROM Timer WHERE Owner=$1", owner) - if err != nil { - log.Fatalln(err) - } - - timers := []Timer{} - for rows.Next() { - var t Timer - if err := rows.Scan(&t.Id, &t.Name); err == nil { - timers = append(timers, t) - } - } - - return timers -} - -func GetTimerForUser(db *sql.DB, id UUID, userId UUID) *Timer { - row := db.QueryRow("SELECT Id, Name, StartTime, EndTime, Owner, Token FROM Timer WHERE Id=$1 AND Owner=$2", id, userId) - - var t Timer - if err := row.Scan(&t.Id, &t.Name, &t.StartTime, &t.EndTime, &t.Owner, &t.Token); err == nil { - return &t - } - - return nil -} - -func GetTimerWithToken(db *sql.DB, id UUID, token string) *Timer { - row := db.QueryRow("SELECT Id, Name, StartTime, EndTime, Owner, Token FROM Timer WHERE Id=$1 AND Token=$2", id, token) - - var t Timer - if err := row.Scan(&t.Id, &t.Name, &t.StartTime, &t.EndTime, &t.Owner, &t.Token); err == nil { - return &t - } - - return nil -} - -func DeleteTimer(db *sql.DB, id UUID, userId UUID) bool { - res, err := db.Exec("DELETE FROM Timer WHERE Id=$1 AND Owner=$2", id, userId) - if err != nil { - return false - } - - affected, err := res.RowsAffected() - return err == nil && affected == 1 -} - -func UpdateTimerEndTime(db *sql.DB, id UUID, endTime Time, userId UUID) bool { - res, err := db.Exec("UPDATE Timer SET EndTime=$1 WHERE Id=$2 AND Owner=$3", endTime, id, userId) - if err != nil { - return false - } - - affected, err := res.RowsAffected() - return err == nil && affected == 1 -} - -func RegenerateTimerToken(db *sql.DB, id UUID, userId UUID) bool { - newToken, err := GenerateTimerToken() - if err != nil { - return false - } - - res, err := db.Exec("UPDATE Timer SET Token=$1 WHERE Id=$2 AND Owner=$3", newToken, id, userId) - if err != nil { - return false - } - - affected, err := res.RowsAffected() - return err == nil && affected == 1 -} @@ -5,8 +5,8 @@ import ( "errors" "net/http" - "stevenlr.com/timer/model" - "stevenlr.com/timer/utils" + "stevenlr.com/locker/model" + "stevenlr.com/locker/utils" ) func generateSessionId() (string, error) { @@ -21,7 +21,7 @@ type Session struct { UserId model.UUID } -const sessionCookieName = "timerSession" +const sessionCookieName = "LockerSession" func removeCookie(cookieName string, w http.ResponseWriter) { cookie := http.Cookie{ diff --git a/static/style.css b/static/style.css index 8ef32c6..01c0910 100644 --- a/static/style.css +++ b/static/style.css @@ -1,9 +1 @@ -body {
- font-size: 16px;
- font-family: sans-serif;
-}
-
-.error {
- color: red;
-}
-
+/*! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#3b82f680;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.contents{display:contents}.hidden{display:none}
\ No newline at end of file diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..4dea733 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ["./view/*.templ"],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+}
diff --git a/view/lock.templ b/view/lock.templ new file mode 100644 index 0000000..4dd0516 --- /dev/null +++ b/view/lock.templ @@ -0,0 +1,80 @@ +package view
+
+import (
+ "fmt"
+ "stevenlr.com/locker/model"
+)
+
+templ LockTokenForm(lock model.Lock) {
+ <p class="token-form">
+ <button
+ type="button"
+ hx-post={ fmt.Sprint("/lock/", lock.Id, "/resetToken") }
+ hx-target="closest .token-form"
+ hx-confirm="Are you sure you want to reset the token for this lock?"
+ >Reset token</button>
+ <button
+ type="button"
+ hx-get={ fmt.Sprint("/lock/", lock.Id, "/token") }
+ hx-swap="outerHTML"
+ >Show token</button>
+ </p>
+}
+
+func renderTimeString(value int, unit string) string {
+ s := ""
+ if value > 1 { s = "s" }
+ return fmt.Sprint(value, " ", unit, s)
+}
+
+templ timeButton(id model.UUID, value int, unit string) {
+ <button
+ hx-target=".lock-info"
+ hx-post={ fmt.Sprint("/lock/", id, "/addTime") }
+ hx-include="next input"
+ >{ renderTimeString(value, unit) }</button>
+ <input type="hidden" name="timeToAdd" value={ fmt.Sprint(value, "", unit[:1]) } />
+}
+
+templ LockInfo(lock model.Lock) {
+ <h1>Lock "{ lock.Name }"</h1>
+ <p>Start time: <local-date>{ lock.StartTime.AsUTCString() }</local-date></p>
+ <p>End time: <local-date>{ lock.EndTime.AsUTCString() }</local-date></p>
+ <p>
+ Total time:
+ <lock-countdown
+ start={ lock.StartTime.AsUTCString() }
+ end={ lock.EndTime.AsUTCString() }
+ ></lock-countdown>
+ </p>
+ <p>
+ Remaining time:
+ <lock-countdown
+ end={ lock.EndTime.AsUTCString() }
+ ></lock-countdown>
+ </p>
+}
+
+templ LockView(lock model.Lock) {
+ <p><a href="/">Back to list</a></p>
+ <div class="lock-info">
+ @LockInfo(lock)
+ </div>
+ if !lock.IsFinished() {
+ <h3>Add time</h3>
+ <p>
+ @timeButton(lock.Id, 15, "minute")
+ @timeButton(lock.Id, 30, "minute")
+ @timeButton(lock.Id, 1, "hour")
+ @timeButton(lock.Id, 2, "hour")
+ @timeButton(lock.Id, 6, "hour")
+ @timeButton(lock.Id, 12, "hour")
+ @timeButton(lock.Id, 1, "day")
+ @timeButton(lock.Id, 1, "week")
+ @timeButton(lock.Id, 4, "week")
+ </p>
+ }
+ <h3>API token</h3>
+ @LockTokenForm(lock)
+}
+
diff --git a/view/locks_list.templ b/view/locks_list.templ new file mode 100644 index 0000000..e940b88 --- /dev/null +++ b/view/locks_list.templ @@ -0,0 +1,51 @@ +package view
+
+import (
+ "stevenlr.com/locker/model"
+ "fmt"
+)
+
+templ lock(t model.Lock) {
+ <p class="lock-row">
+ <a href={ templ.URL(fmt.Sprint("/lock/", t.Id)) }>{ t.Name }</a>
+ -
+ <a
+ href="javascript:void(0);"
+ hx-delete={ fmt.Sprint("/lock/", t.Id) }
+ hx-target="closest .lock-row"
+ hx-confirm={ fmt.Sprint("Are you sure you want to delete lock \"", t.Name , "\"?") }
+ >Delete</a>
+ </p>
+}
+
+templ LockCreateForm(lockName string, err string) {
+ <form
+ hx-put="/lock"
+ hx-target="closest .locks-list"
+ hx-target-error="this"
+ >
+ <p>
+ <input type="text" name="lockName" value={ lockName } placeholder="Name" />
+ <input type="number" name="days" placeholder="Days" />
+ <input type="number" name="hours" placeholder="Hours" />
+ <button type="submit">Create</button>
+ </p>
+ if err != "" {
+ <p class="error">{ err }</p>
+ }
+ </form>
+}
+
+templ LocksList(locks []model.Lock, isSignedIn bool) {
+ <div class="locks-list">
+ if isSignedIn {
+ <h1>Locks</h1>
+ for _, t := range locks {
+ @lock(t)
+ }
+ <h4>Create lock</h4>
+ @LockCreateForm("", "")
+ }
+ </div>
+}
+
diff --git a/view/login.templ b/view/login.templ index 3450e01..56e003c 100644 --- a/view/login.templ +++ b/view/login.templ @@ -1,7 +1,7 @@ package view
import (
- "stevenlr.com/timer/model"
+ "stevenlr.com/locker/model"
)
templ LoginFormError(currentUser *model.User, err string) {
diff --git a/view/main.templ b/view/main.templ index 06cb872..4ae917b 100644 --- a/view/main.templ +++ b/view/main.templ @@ -1,14 +1,14 @@ package view
import (
- "stevenlr.com/timer/model"
+ "stevenlr.com/locker/model"
)
templ Main(contents templ.Component, currentUser *model.User) {
<!DOCTYPE html>
<html>
<head>
- <title>Cool timer app</title>
+ <title>Cool locker app</title>
<link rel="stylesheet" href="/static/style.css" />
<script type="module" src="/static/ui-components.js"></script>
<script src="/static/htmx.min.js"></script>
diff --git a/view/timer.templ b/view/timer.templ deleted file mode 100644 index 09b1345..0000000 --- a/view/timer.templ +++ /dev/null @@ -1,80 +0,0 @@ -package view
-
-import (
- "fmt"
- "stevenlr.com/timer/model"
-)
-
-templ TimerTokenForm(timer model.Timer) {
- <p class="token-form">
- <button
- type="button"
- hx-post={ fmt.Sprint("/timer/", timer.Id, "/resetToken") }
- hx-target="closest .token-form"
- hx-confirm="Are you sure you want to reset the token for this timer?"
- >Reset token</button>
- <button
- type="button"
- hx-get={ fmt.Sprint("/timer/", timer.Id, "/token") }
- hx-swap="outerHTML"
- >Show token</button>
- </p>
-}
-
-func renderTimeString(value int, unit string) string {
- s := ""
- if value > 1 { s = "s" }
- return fmt.Sprint(value, " ", unit, s)
-}
-
-templ timeButton(id model.UUID, value int, unit string) {
- <button
- hx-target=".timer-info"
- hx-post={ fmt.Sprint("/timer/", id, "/addTime") }
- hx-include="next input"
- >{ renderTimeString(value, unit) }</button>
- <input type="hidden" name="timeToAdd" value={ fmt.Sprint(value, "", unit[:1]) } />
-}
-
-templ TimerInfo(timer model.Timer) {
- <h1>Timer "{ timer.Name }"</h1>
- <p>Start time: <local-date>{ timer.StartTime.AsUTCString() }</local-date></p>
- <p>End time: <local-date>{ timer.EndTime.AsUTCString() }</local-date></p>
- <p>
- Total time:
- <timer-countdown
- start={ timer.StartTime.AsUTCString() }
- end={ timer.EndTime.AsUTCString() }
- ></timer-countdown>
|