all repos — well-binge @ 4690b04bb57834544694aa1b0d740fe746ec7236

Create positive, recurring habits.

add edit page, action buttons
Marco Andronaco andronacomarco@gmail.com
Thu, 17 Oct 2024 23:54:44 +0200
commit

4690b04bb57834544694aa1b0d740fe746ec7236

parent

55e48ab1355d3b02788c0881ffa8067493ecfb6b

M src/app/functions.gosrc/app/functions.go

@@ -8,6 +8,7 @@ "log"

"net/http" "os" "regexp" + "strconv" "strings" "time"

@@ -15,6 +16,7 @@ "github.com/birabittoh/auth-boilerplate/src/email"

) type HabitDisplay struct { + ID uint Class string Name string LastAck string

@@ -141,13 +143,42 @@ }

func getLoggedUser(r *http.Request) (user User, ok bool) { userID, ok := r.Context().Value(userContextKey).(uint) - db.Find(&user, userID) + if !ok { + return + } + + if db.Find(&user, userID).Error != nil { + ok = true + } + return user, ok } -func formatDuration(d time.Duration) string { - // TODO: 48h1m13s --> 2.01 days - return d.String() +func formatDuration(t time.Time) string { + days := int(time.Since(t).Hours()) / 24 + + switch { + case days == 0: + return "Today" + case days == 1: + return "Yesterday" + case days <= 7: + return fmt.Sprintf("%d day(s) ago", days) + case days <= 30: + weeks := days / 7 + remainingDays := days % 7 + if remainingDays == 0 { + return fmt.Sprintf("%d week(s) ago", weeks) + } + return fmt.Sprintf("%d wee(k), %d day(s) ago", weeks, remainingDays) + default: + months := days / 30 + remainingDays := days % 30 + if remainingDays == 0 { + return fmt.Sprintf("%d month(s) ago", months) + } + return fmt.Sprintf("%d month(s), %d day(s) ago", months, remainingDays) + } } func toHabitDisplay(habit Habit) HabitDisplay {

@@ -155,9 +186,11 @@ var lastAck string

if habit.LastAck == nil { lastAck = "-" } else { - lastAck = formatDuration(time.Since(*habit.LastAck)) + lastAck = formatDuration(*habit.LastAck) } + return HabitDisplay{ + ID: habit.ID, Name: habit.Name, LastAck: lastAck, Disabled: habit.Disabled,

@@ -165,6 +198,39 @@ Class: classGood,

} } +func getHabit(id uint) (habit Habit, err error) { + err = db.Model(&Habit{}).Find(&habit, id).Error + return +} + +func getHabitHelper(w http.ResponseWriter, r *http.Request) (habit Habit, err error) { + id := getID(r) + if id == 0 { + err = errors.New("no id") + http.Error(w, "bad request", http.StatusBadRequest) + return + } + + user, ok := getLoggedUser(r) + if !ok { + err = errors.New("no logged user") + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + habit, err = getHabit(id) + if err != nil { + http.Error(w, "not found", http.StatusNotFound) + return + } + + if habit.UserID != user.ID { + err = errors.New("forbidden") + http.Error(w, "forbidden", http.StatusForbidden) + } + return +} + func getAllHabits(userID uint) (positives []HabitDisplay, negatives []HabitDisplay, err error) { var habits []Habit err = db.Model(&Habit{}).Where(&Habit{UserID: userID}).Find(&habits).Error

@@ -182,3 +248,11 @@ }

} return } + +func getID(r *http.Request) uint { + res, err := strconv.ParseUint(r.PathValue("id"), 10, 64) + if err != nil { + return 0 + } + return uint(res) +}
M src/app/handlers.gosrc/app/handlers.go

@@ -32,6 +32,15 @@

xt.ExecuteTemplate(w, "habits.tmpl", data) } +func getHabitsIDHandler(w http.ResponseWriter, r *http.Request) { + habit, err := getHabitHelper(w, r) + if err != nil { + return + } + + xt.ExecuteTemplate(w, "habits-id.tmpl", habit) +} + func getNewPositiveHandler(w http.ResponseWriter, r *http.Request) { data := map[string]interface{}{"Negative": false} xt.ExecuteTemplate(w, "new.tmpl", data)

@@ -71,6 +80,28 @@ Name: name,

Days: days, Negative: negative, }) + + http.Redirect(w, r, "/habits", http.StatusFound) +} + +func postAckIDHandler(w http.ResponseWriter, r *http.Request) { + habit, err := getHabitHelper(w, r) + if err != nil { + return + } + + if habit.LastAck != nil { + if time.Since(*habit.LastAck) < 6*time.Hour { + http.Redirect(w, r, "/habits", http.StatusFound) // TODO: redirect to an error page instead + return + } + } + + db.Create(Ack{HabitID: habit.ID}) + + now := time.Now() + habit.LastAck = &now + db.Save(&habit) http.Redirect(w, r, "/habits", http.StatusFound) }
M src/app/init.gosrc/app/init.go

@@ -118,9 +118,11 @@

// App http.HandleFunc("GET /", getIndexHandler) http.HandleFunc("GET /habits", loginRequired(getHabitsHandler)) + http.HandleFunc("GET /habits/{id}", loginRequired(getHabitsIDHandler)) http.HandleFunc("GET /new/positive", loginRequired(getNewPositiveHandler)) http.HandleFunc("GET /new/negative", loginRequired(getNewNegativeHandler)) http.HandleFunc("POST /new", loginRequired(postNewHandler)) + http.HandleFunc("POST /ack/{id}", loginRequired(postAckIDHandler)) // Auth http.HandleFunc("GET /register", getRegisterHandler)
M static/style.cssstatic/style.css

@@ -18,3 +18,14 @@ grid-template-columns: auto auto;

justify-content: space-between; align-items: center } + +.actions { + display: grid; + grid-template-columns: auto auto; + justify-items: center; + justify-content: center; +} + +.actions > form { + padding-inline: 5px; +}
M templates/auth-login.tmpltemplates/auth-login.tmpl

@@ -1,6 +1,6 @@

{{ extends "auth.tmpl" }} -{{define "title" -}}Login{{end}} +{{define "title" -}}Login - {{end}} {{define "auth" -}} <h1>Login</h1>
M templates/auth-new_password.tmpltemplates/auth-new_password.tmpl

@@ -1,6 +1,6 @@

{{ extends "auth.tmpl" }} -{{define "title" -}}Reset password{{end}} +{{define "title" -}}Reset password - {{end}} {{define "auth" -}} <h1>Reset password</h1>
M templates/auth-register.tmpltemplates/auth-register.tmpl

@@ -1,6 +1,6 @@

{{ extends "auth.tmpl" }} -{{define "title" -}}Sign up{{end}} +{{define "title" -}}Sign up - {{end}} {{define "auth" -}} <h1>Sign up</h1>
M templates/auth-reset_password.tmpltemplates/auth-reset_password.tmpl

@@ -1,6 +1,6 @@

{{ extends "auth.tmpl" }} -{{define "title" -}}Reset password{{end}} +{{define "title" -}}Reset password - {{end}} {{define "auth" -}} <h1>Reset password</h1>
M templates/base.tmpltemplates/base.tmpl

@@ -4,7 +4,7 @@

<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title>{{block "title" .}}Page title{{end}}</title> + <title>{{block "title" .}}{{end}}WellBinge</title> <link rel="icon" type="image/png" href="/static/favicon/favicon-48x48.png" sizes="48x48" /> <link rel="icon" type="image/svg+xml" href="/static/favicon/favicon.svg" />
A templates/habits-id.tmpl

@@ -0,0 +1,28 @@

+{{ extends "base.tmpl" }} + +{{define "title" -}}Edit - {{end}} + +{{define "content" -}} + <h1>Edit habit</h1> + + <form method="post" action="/edit"> + <label> + <span>Name:</span> + <input type="text" name="name" autocomplete="off" placeholder="Name" value="{{ .Name }}" required /> + </label> + {{ if not .Negative }} + <label> + <span>Days:</span> + <input type="number" name="days" autocomplete="off" placeholder="Days" min="1" max="60" value="{{ .Days }}" required /> + </label> + <label> + <span>Enabled:</span> + <input type="checkbox" name="enabled"{{ if not .Disabled }} checked{{ end }} /> + </label> + {{ end }} + <input type="submit" value="Edit" /> + </form> + <form method="post" action="/edit"> + <input type="submit" value="Delete" /> + </form> +{{end}}
M templates/habits.tmpltemplates/habits.tmpl

@@ -1,12 +1,12 @@

{{ extends "base.tmpl" }} -{{define "title" -}}Habits{{end}} +{{define "title" -}}Habits - {{end}} {{define "content" -}} <h1>Welcome, <i>{{.User.Username}}</i>!</h1> <a href="/logout">Logout</a><br /> <div class="habits-title"> - <h4>Positive habits</h4> + <h3>Positive habits</h3> <a href="/new/positive" class="button">+ Add</a> </div> <table>

@@ -14,23 +14,33 @@ <thead>

<tr> <td>Name</td> <td>Last time</td> - <td>Enabled</td> + <td>Actions</td> </tr> </thead> <tbody> - {{range .Positive}} - <tr class="{{.Class}}"> - <td>{{.Name}}</td> - <td>{{.LastAck}}</td> - <td><input type="checkbox" disabled{{ if not .Disabled }} checked{{end}} /></td> - </tr> + {{ range .Positive }} + <a href="/habits/{{ .ID }}"> + <tr class="{{.Class}}"> + <td>{{ .Name }}</td> + <td><i>{{ .LastAck }}</i></td> + <td class="actions"> + <form action="/ack/{{ .ID }}" method="post"> + <input type="submit" value="Ack" /> + </form> + + <form action="/habits/{{ .ID }}" method="get"> + <input type="submit" value="Edit" /> + </form> + </td> + </tr> + </a> {{end}} </tbody> <tfoot></tfoot> </table> <div class="habits-title"> - <h4>Negative habits</h4> + <h3>Negative habits</h3> <a href="/new/negative" class="button">+ Add</a> </div> <table>

@@ -38,6 +48,7 @@ <thead>

<tr> <td>Name</td> <td>Last time</td> + <td>Actions</td> </tr> </thead> <tbody>

@@ -45,6 +56,15 @@ {{range .Negative}}

<tr class="{{.Class}}"> <td>{{.Name}}</td> <td>{{.LastAck}}</td> + <td class="actions"> + <form action="/ack/{{ .ID }}" method="post"> + <input type="submit" value="Ack" /> + </form> + + <form action="/habits/{{ .ID }}" method="get"> + <input type="submit" value="Edit" /> + </form> + </td> </tr> {{end}} </tbody>
M templates/index.tmpltemplates/index.tmpl

@@ -1,7 +1,5 @@

{{ extends "base.tmpl" }} -{{define "title" -}}WellBinge{{end}} - {{define "content" -}} <h1>WellBinge</h1> <h4>Create positive habits, get reminders, quit addictions.</h4>
M templates/new.tmpltemplates/new.tmpl

@@ -1,6 +1,6 @@

{{ extends "base.tmpl" }} -{{define "title" -}}New habit{{end}} +{{define "title" -}}New - {{end}} {{define "content" -}} <h1>New habit</h1>