summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSteven Le Rouzic <steven.lerouzic@gmail.com>2024-04-27 23:55:31 +0200
committerSteven Le Rouzic <steven.lerouzic@gmail.com>2024-04-27 23:55:31 +0200
commita55cfe8205d5cb2fb0948c173f23ad71d6614d13 (patch)
tree386a341b3bcc543ca8ac7bfe7ff944cac669fe25
parentdf3068728abacfc98fa19f3dba62b35f65aea731 (diff)
Rename everything to locker/lock
-rw-r--r--build.bat4
-rw-r--r--css/input.css3
-rw-r--r--database.go6
-rw-r--r--go.mod2
-rw-r--r--lock.db (renamed from timer.db)bin28672 -> 28672 bytes
-rw-r--r--locker.go (renamed from timer.go)136
-rw-r--r--model/lock.go110
-rw-r--r--model/timer.go110
-rw-r--r--session.go6
-rw-r--r--static/style.css10
-rw-r--r--tailwind.config.js8
-rw-r--r--view/lock.templ80
-rw-r--r--view/locks_list.templ51
-rw-r--r--view/login.templ2
-rw-r--r--view/main.templ4
-rw-r--r--view/timer.templ80
-rw-r--r--view/timers_list.templ51
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
}
diff --git a/go.mod b/go.mod
index ec76474..7723c83 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module stevenlr.com/timer
+module stevenlr.com/locker
go 1.22.2
diff --git a/timer.db b/lock.db
index 606b560..a5c6824 100644
--- a/timer.db
+++ b/lock.db
Binary files differ
diff --git a/timer.go b/locker.go
index 4b29726..7bc6cc4 100644
--- a/timer.go
+++ b/locker.go
@@ -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
-}
diff --git a/session.go b/session.go
index e32041f..9d86bc7 100644
--- a/session.go
+++ b/session.go
@@ -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>