Rename everything to locker/lock

This commit is contained in:
2024-04-27 23:55:31 +02:00
parent df3068728a
commit a55cfe8205
17 changed files with 335 additions and 328 deletions

4
build.bat Normal file
View File

@ -0,0 +1,4 @@
templ generate
tailwindcss -i css\input.css -o static\style.css --minify

3
css/input.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@ -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
}

2
go.mod
View File

@ -1,4 +1,4 @@
module stevenlr.com/timer
module stevenlr.com/locker
go 1.22.2

Binary file not shown.

View File

@ -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!")

110
model/lock.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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{

View File

@ -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}

8
tailwind.config.js Normal file
View File

@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./view/*.templ"],
theme: {
extend: {},
},
plugins: [],
}

80
view/lock.templ Normal file
View File

@ -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)
}

51
view/locks_list.templ Normal file
View File

@ -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>
}

View File

@ -1,7 +1,7 @@
package view
import (
"stevenlr.com/timer/model"
"stevenlr.com/locker/model"
)
templ LoginFormError(currentUser *model.User, err string) {

View File

@ -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>

View File

@ -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>
</p>
<p>
Remaining time:
<timer-countdown
end={ timer.EndTime.AsUTCString() }
></timer-countdown>
</p>
}
templ TimerView(timer model.Timer) {
<p><a href="/">Back to list</a></p>
<div class="timer-info">
@TimerInfo(timer)
</div>
if !timer.IsFinished() {
<h3>Add time</h3>
<p>
@timeButton(timer.Id, 15, "minute")
@timeButton(timer.Id, 30, "minute")
@timeButton(timer.Id, 1, "hour")
@timeButton(timer.Id, 2, "hour")
@timeButton(timer.Id, 6, "hour")
@timeButton(timer.Id, 12, "hour")
@timeButton(timer.Id, 1, "day")
@timeButton(timer.Id, 1, "week")
@timeButton(timer.Id, 4, "week")
</p>
}
<h3>API token</h3>
@TimerTokenForm(timer)
}

View File

@ -1,51 +0,0 @@
package view
import (
"stevenlr.com/timer/model"
"fmt"
)
templ timer(t model.Timer) {
<p class="timer-row">
<a href={ templ.URL(fmt.Sprint("/timer/", t.Id)) }>{ t.Name }</a>
-
<a
href="javascript:void(0);"
hx-delete={ fmt.Sprint("/timer/", t.Id) }
hx-target="closest .timer-row"
hx-confirm={ fmt.Sprint("Are you sure you want to delete timer \"", t.Name , "\"?") }
>Delete</a>
</p>
}
templ TimerCreateForm(timerName string, err string) {
<form
hx-put="/timer"
hx-target="closest .timers-list"
hx-target-error="this"
>
<p>
<input type="text" name="timerName" value={ timerName } placeholder="Name" />
<input type="number" name="days" placeholder="Days" style="width: 5em;" />
<input type="number" name="hours" placeholder="Hours" style="width: 5em;" />
<button type="submit">Create</button>
</p>
if err != "" {
<p class="error">{ err }</p>
}
</form>
}
templ TimersList(timers []model.Timer, isSignedIn bool) {
<div class="timers-list">
if isSignedIn {
<h1>Timers</h1>
for _, t := range timers {
@timer(t)
}
<h4>Create timer</h4>
@TimerCreateForm("", "")
}
</div>
}