Compare commits

..

6 Commits

Author SHA1 Message Date
Muyao CHEN
dc7ece7d12 update go mod 2024-07-01 14:34:15 +02:00
Muyao CHEN
d8d2abcde3 Finishing up our response to user, and adding alerts 2024-07-01 14:28:00 +02:00
Muyao CHEN
3ad57c754f Displaying a response to user after posting form data 2024-07-01 14:19:38 +02:00
Muyao CHEN
6826634a01 Server Side form validation 4: check email 2024-07-01 13:47:52 +02:00
Muyao CHEN
87dfd26268 Server Side form validation 3 2024-07-01 13:37:03 +02:00
Muyao CHEN
8be6ba7119 Server side form validation 2 2024-06-30 19:35:59 +02:00
11 changed files with 189 additions and 6 deletions

View File

@ -1,9 +1,11 @@
package main package main
import ( import (
"encoding/gob"
"fmt" "fmt"
"go-udemy-web-1/internal/config" "go-udemy-web-1/internal/config"
"go-udemy-web-1/internal/handlers" "go-udemy-web-1/internal/handlers"
"go-udemy-web-1/internal/models"
"go-udemy-web-1/internal/render" "go-udemy-web-1/internal/render"
"log" "log"
"net/http" "net/http"
@ -21,6 +23,9 @@ var (
// main is the main application function // main is the main application function
func main() { func main() {
// what am I going to put in the session
gob.Register(models.Reservation{})
// change this to true when in production // change this to true when in production
app.InProduction = false app.InProduction = false

View File

@ -27,6 +27,7 @@ func routes(app *config.AppConfig) http.Handler {
mux.Post("/availability-json", handlers.Repo.AvailabilityJSON) mux.Post("/availability-json", handlers.Repo.AvailabilityJSON)
mux.Get("/make-reservation", handlers.Repo.MakeReservation) mux.Get("/make-reservation", handlers.Repo.MakeReservation)
mux.Post("/make-reservation", handlers.Repo.PostMakeReservation) mux.Post("/make-reservation", handlers.Repo.PostMakeReservation)
mux.Get("/reservation-summary", handlers.Repo.ReservationSummary)
fileServer := http.FileServer(http.Dir("./static/")) fileServer := http.FileServer(http.Dir("./static/"))
mux.Handle("/static/*", http.StripPrefix("/static", fileServer)) mux.Handle("/static/*", http.StripPrefix("/static", fileServer))

4
go.mod
View File

@ -2,8 +2,12 @@ module go-udemy-web-1
go 1.21.0 go 1.21.0
// github.com/CloudyKit/jet --> Check this later
require github.com/go-chi/chi/v5 v5.0.14 require github.com/go-chi/chi/v5 v5.0.14
require github.com/justinas/nosurf v1.1.1 require github.com/justinas/nosurf v1.1.1
require github.com/alexedwards/scs/v2 v2.8.0 require github.com/alexedwards/scs/v2 v2.8.0
require github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2

2
go.sum
View File

@ -1,5 +1,7 @@
github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw= github.com/alexedwards/scs/v2 v2.8.0 h1:h31yUYoycPuL0zt14c0gd+oqxfRwIj6SOjHdKRZxhEw=
github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= github.com/alexedwards/scs/v2 v2.8.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/go-chi/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0= github.com/go-chi/chi/v5 v5.0.14 h1:PyEwo2Vudraa0x/Wl6eDRRW2NXBvekgfxyydcM0WGE0=
github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.14/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk= github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk=

View File

@ -1,8 +1,12 @@
package forms package forms
import ( import (
"fmt"
"net/http" "net/http"
"net/url" "net/url"
"strings"
"github.com/asaskevich/govalidator"
) )
// Form creates a custom form struct, embeds a url.Values object // Form creates a custom form struct, embeds a url.Values object
@ -19,8 +23,41 @@ func New(data url.Values) *Form {
} }
} }
// Required checks required fields
func (f *Form) Required(fields ...string) {
for _, field := range fields {
value := f.Get(field)
if strings.TrimSpace(value) == "" {
f.Errors.Add(field, "This field cannot be blank")
}
}
}
// Has checks if form field is in post and not emtpy // Has checks if form field is in post and not emtpy
func (f *Form) Has(field string, r *http.Request) bool { func (f *Form) Has(field string, r *http.Request) bool {
x := r.Form.Get(field) x := r.Form.Get(field)
return x != "" return x != ""
}
// Valid returns true if there are no errors, otherwise false
func (f *Form) Valid() bool {
return len(f.Errors) == 0
}
// MinLength checks for string minimum length
func (f *Form) MinLength(field string, length int, r *http.Request) bool {
value := r.Form.Get(field)
if len(value) < length {
f.Errors.Add(field, fmt.Sprintf("This field must have at least %d letters", length))
return false
}
return true
}
// IsEmail checks the email address
func (f *Form) IsEmail(field string, r *http.Request) {
value := r.Form.Get(field)
if !govalidator.IsEmail(value) {
f.Errors.Add(field, "Invalid email address")
}
} }

View File

@ -69,13 +69,73 @@ func (m *Repository) Majors(w http.ResponseWriter, r *http.Request) {
// MakeReservation is the make reservation page handler // MakeReservation is the make reservation page handler
func (m *Repository) MakeReservation(w http.ResponseWriter, r *http.Request) { func (m *Repository) MakeReservation(w http.ResponseWriter, r *http.Request) {
// For the first time render emptyReservation so that this object is
// filled with the info when sent back.
var emptyReservation models.Reservation
data := make(map[string]interface{})
data["reservation"] = emptyReservation
render.RenderTemplate(w, r, "make-reservation.page.tmpl", &models.TemplateData{ render.RenderTemplate(w, r, "make-reservation.page.tmpl", &models.TemplateData{
Form: forms.New(nil), Form: forms.New(nil),
Data: data,
}) })
} }
// PostMakeReservation is the make reservation page post handler // PostMakeReservation is the make reservation page post handler
func (m *Repository) PostMakeReservation(w http.ResponseWriter, r *http.Request) { func (m *Repository) PostMakeReservation(w http.ResponseWriter, r *http.Request) {
err := r.ParseForm()
if err != nil {
log.Println(err)
return
}
reservation := models.Reservation{
FirstName: r.Form.Get("first_name"),
LastName: r.Form.Get("last_name"),
Email: r.Form.Get("email"),
Phone: r.Form.Get("phone"),
}
form := forms.New(r.PostForm)
form.Required("first_name", "last_name", "email")
form.MinLength("first_name", 2, r)
form.IsEmail("email", r)
if !form.Valid() {
data := make(map[string]interface{})
data["reservation"] = reservation
render.RenderTemplate(w, r, "make-reservation.page.tmpl", &models.TemplateData{
Data: data,
Form: form,
})
return
}
m.App.Session.Put(r.Context(), "reservation", reservation)
http.Redirect(w, r, "/reservation-summary", http.StatusSeeOther)
}
// ReservationSummary is the reservation summary page handler
func (m *Repository) ReservationSummary(w http.ResponseWriter, r *http.Request) {
reservation, ok := m.App.Session.Get(r.Context(), "reservation").(models.Reservation)
if !ok {
log.Println("connot get item from reservation")
m.App.Session.Put(r.Context(), "error", "Can't get reservation from session")
http.Redirect(w, r, "/", http.StatusTemporaryRedirect)
}
m.App.Session.Remove(r.Context(), "reservation")
data := make(map[string]interface{})
data["reservation"] = reservation
render.RenderTemplate(w, r, "reservation-summary.page.tmpl", &models.TemplateData{
Data: data,
})
} }
// Availability is the search for availability page handler // Availability is the search for availability page handler

View File

@ -0,0 +1,9 @@
package models
// Reservation holds Reservation data
type Reservation struct {
FirstName string
LastName string
Email string
Phone string
}

View File

@ -21,6 +21,9 @@ func NewTemplates(a *config.AppConfig) {
// AddDefaultData adds default template data // AddDefaultData adds default template data
func AddDefaultData(td *models.TemplateData, r *http.Request) *models.TemplateData { func AddDefaultData(td *models.TemplateData, r *http.Request) *models.TemplateData {
td.Flash = app.Session.PopString(r.Context(), "flash")
td.Warning = app.Session.PopString(r.Context(), "warning")
td.Error = app.Session.PopString(r.Context(), "error")
td.CSRFToken = nosurf.Token(r) td.CSRFToken = nosurf.Token(r)
return td return td
} }

View File

@ -116,6 +116,16 @@
}) })
} }
{{with .Error}}
notify("{{.}}", "error")
{{end}}
{{with .Warning}}
notify("{{.}}", "warning")
{{end}}
{{with .Flash}}
notify("{{.}}", "flash")
{{end}}
// Prompt is out Javascript module for all alerts, notifications, and custom popup dialogs // Prompt is out Javascript module for all alerts, notifications, and custom popup dialogs
function Prompt() { function Prompt() {
let toast = function (c) { let toast = function (c) {

View File

@ -5,25 +5,38 @@
<div class="col"> <div class="col">
<h1 class="text-center mt-3">Make reservation</h1> <h1 class="text-center mt-3">Make reservation</h1>
{{$res := index .Data "reservation"}}
<!-- <form method="post" action="" class="needs-validation" novalidate> --> <!-- <form method="post" action="" class="needs-validation" novalidate> -->
<form method="post" action="" class="" novalidate> <form method="post" action="" class="" novalidate>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}"> <input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<div class="form-group mt-5"> <div class="form-group mt-5">
<label for="first_name">First name:</label> <label for="first_name">First name:</label>
<input type="text" name="first_name" id="first_name" class="form-control" required {{with .Form.Errors.Get "first_name"}}
autocomplete="off"> <label class="text-danger">{{.}}</label>
{{end}}
<input type="text" name="first_name" id="first_name" class="form-control {{with .Form.Errors.Get "first_name"}} is-invalid {{end}}"
value="{{$res.FirstName}}" required autocomplete="off">
</div> </div>
<div class="form-group mt-5"> <div class="form-group mt-5">
<label for="last_name">Last name:</label> <label for="last_name">Last name:</label>
<input type="text" name="last_name" id="last_name" class="form-control" required autocomplete="off"> {{with .Form.Errors.Get "last_name"}}
<label class="text-danger">{{.}}</label>
{{end}}
<input type="text" name="last_name" id="last_name" class="form-control {{with .Form.Errors.Get "last_name"}} is-invalid {{end}}"
value="{{$res.LastName}}" required autocomplete="off">
</div> </div>
<div class="form-group mt-5"> <div class="form-group mt-5">
<label for="email">Email:</label> <label for="email">Email:</label>
<input type="email" name="email" id="email" class="form-control" required autocomplete="off"> {{with .Form.Errors.Get "email"}}
<label class="text-danger">{{.}}</label>
{{end}}
<input type="email" name="email" id="email" class="form-control {{with .Form.Errors.Get "email"}} is-invalid {{end}}"
value="{{$res.Email}}" required autocomplete="off">
</div> </div>
<div class="form-group mt-5"> <div class="form-group mt-5">
<label for="phone">Phone number:</label> <label for="phone">Phone number:</label>
<input type="text" name="phone" id="phone" class="form-control" required autocomplete="off"> <input type="text" name="phone" id="phone" class="form-control" value="{{$res.Phone}}" autocomplete="off">
</div> </div>
<hr> <hr>

View File

@ -0,0 +1,39 @@
{{template "base" .}}
{{define "content"}}
{{$res := index .Data "reservation"}}
<div class="container">
<div class="row">
<div class="col mt-5">
<h1>Reservation Summary</h1>
<hr>
<table class="table table-striped">
<thead></thead>
<tbody>
<tr>
<td>Name: </td>
<td>{{$res.LastName}} {{$res.FirstName}}</td>
</tr>
<tr>
<td>Arrival: </td>
<td></td>
</tr>
<tr>
<td>Departure: </td>
<td></td>
</tr>
<tr>
<td>Email: </td>
<td>{{$res.Email}}</td>
</tr>
<tr>
<td>Phone: </td>
<td>{{$res.Phone}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
{{end}}