Compare commits

...

8 Commits

Author SHA1 Message Date
Muyao CHEN
8394832428 Server-side form validation 1 2024-06-30 17:08:47 +02:00
Muyao CHEN
7294254e13 Refactoring to use internal packages 2024-06-30 16:41:46 +02:00
Muyao CHEN
eca62e2e7b Sending AJAX post and generalizing the custom function 2024-06-30 16:37:57 +02:00
Muyao CHEN
e18849331c Sending & processing an AJAX request 2024-06-30 16:23:12 +02:00
Muyao CHEN
97b9898dfc rename reservation to availability 2024-06-30 10:51:29 +02:00
Muyao CHEN
a8b56b50db Creating a handler that return JSON 2024-06-30 10:42:57 +02:00
Muyao CHEN
76bee566cd Creating handlers for forms & adding CSRF Protection 2024-06-30 10:31:15 +02:00
Muyao CHEN
592d5241d1 Add check-avalability js 2024-06-29 21:42:15 +02:00
14 changed files with 271 additions and 124 deletions

View File

@ -2,9 +2,9 @@ package main
import (
"fmt"
"go-udemy-web-1/pkg/config"
"go-udemy-web-1/pkg/handlers"
"go-udemy-web-1/pkg/render"
"go-udemy-web-1/internal/config"
"go-udemy-web-1/internal/handlers"
"go-udemy-web-1/internal/render"
"log"
"net/http"
"time"

View File

@ -1,8 +1,8 @@
package main
import (
"go-udemy-web-1/pkg/config"
"go-udemy-web-1/pkg/handlers"
"go-udemy-web-1/internal/config"
"go-udemy-web-1/internal/handlers"
"net/http"
"github.com/go-chi/chi/v5"
@ -22,8 +22,11 @@ func routes(app *config.AppConfig) http.Handler {
mux.Get("/contact", handlers.Repo.Contact)
mux.Get("/generals-quarters", handlers.Repo.Generals)
mux.Get("/majors-suite", handlers.Repo.Majors)
mux.Get("/reservation", handlers.Repo.Reservation)
mux.Get("/availability", handlers.Repo.Availability)
mux.Post("/availability", handlers.Repo.PostAvailability)
mux.Post("/availability-json", handlers.Repo.AvailabilityJSON)
mux.Get("/make-reservation", handlers.Repo.MakeReservation)
mux.Post("/make-reservation", handlers.Repo.PostMakeReservation)
fileServer := http.FileServer(http.Dir("./static/"))
mux.Handle("/static/*", http.StripPrefix("/static", fileServer))

18
internal/forms/errors.go Normal file
View File

@ -0,0 +1,18 @@
package forms
type errors map[string][]string
// Add adds an error message for a given form field
func (e errors) Add(field, message string) {
e[field] = append(e[field], message)
}
// Get returns the first error message
func (e errors) Get(field string) string {
es := e[field]
if len(es) == 0 {
return ""
}
return es[0]
}

26
internal/forms/forms.go Normal file
View File

@ -0,0 +1,26 @@
package forms
import (
"net/http"
"net/url"
)
// Form creates a custom form struct, embeds a url.Values object
type Form struct {
url.Values
Errors errors
}
// New initializes a form struct
func New(data url.Values) *Form {
return &Form{
data,
errors(map[string][]string{}),
}
}
// Has checks if form field is in post and not emtpy
func (f *Form) Has(field string, r *http.Request) bool {
x := r.Form.Get(field)
return x != ""
}

View File

@ -0,0 +1,113 @@
package handlers
import (
"encoding/json"
"fmt"
"go-udemy-web-1/internal/config"
"go-udemy-web-1/internal/forms"
"go-udemy-web-1/internal/models"
"go-udemy-web-1/internal/render"
"log"
"net/http"
)
// Repo the repository used by the handlers
var Repo *Repository
// Repository is the repository type
type Repository struct {
App *config.AppConfig
}
// NewRepo creates a new repository
func NewRepo(a *config.AppConfig) *Repository {
return &Repository{
App: a,
}
}
// NewHandlers sets the repository for the handlers
func NewHandlers(r *Repository) {
Repo = r
}
// Home is the home page handler
func (m *Repository) Home(w http.ResponseWriter, r *http.Request) {
remoteIP := r.RemoteAddr
m.App.Session.Put(r.Context(), "remote_ip", remoteIP)
render.RenderTemplate(w, r, "home.page.tmpl", &models.TemplateData{})
}
// About is the about page handler
func (m *Repository) About(w http.ResponseWriter, r *http.Request) {
// perform some logic
stringMap := make(map[string]string)
stringMap["test"] = "Hello world!"
remoteIP := m.App.Session.GetString(r.Context(), "remote_ip")
stringMap["remote_ip"] = remoteIP
// send the data to the template
render.RenderTemplate(w, r, "about.page.tmpl", &models.TemplateData{StringMap: stringMap})
}
// Contact is the contact page handler
func (m *Repository) Contact(w http.ResponseWriter, r *http.Request) {
render.RenderTemplate(w, r, "contact.page.tmpl", &models.TemplateData{})
}
// Generals is the General's Quarters page handler
func (m *Repository) Generals(w http.ResponseWriter, r *http.Request) {
render.RenderTemplate(w, r, "generals.page.tmpl", &models.TemplateData{})
}
// Majors is the Major's Suite page handler
func (m *Repository) Majors(w http.ResponseWriter, r *http.Request) {
render.RenderTemplate(w, r, "majors.page.tmpl", &models.TemplateData{})
}
// MakeReservation is the make reservation page handler
func (m *Repository) MakeReservation(w http.ResponseWriter, r *http.Request) {
render.RenderTemplate(w, r, "make-reservation.page.tmpl", &models.TemplateData{
Form: forms.New(nil),
})
}
// PostMakeReservation is the make reservation page post handler
func (m *Repository) PostMakeReservation(w http.ResponseWriter, r *http.Request) {
}
// Availability is the search for availability page handler
func (m *Repository) Availability(w http.ResponseWriter, r *http.Request) {
render.RenderTemplate(w, r, "availability.page.tmpl", &models.TemplateData{})
}
// PostAvailability is the search for availability page handler
func (m *Repository) PostAvailability(w http.ResponseWriter, r *http.Request) {
start := r.Form.Get("start")
end := r.Form.Get("end")
fmt.Fprintf(w, "Posted to search availability from %s to %s", start, end)
}
type responseJSON struct {
OK string `json:"ok"`
Message string `json:"message"`
}
// AvailabilityJSON is the search for availability page handler
func (m *Repository) AvailabilityJSON(w http.ResponseWriter, r *http.Request) {
resp := responseJSON{
OK: "true",
Message: "Available!",
}
out, err := json.MarshalIndent(resp, "", " ")
if err != nil {
log.Println(err)
}
w.Header().Set("Content-Type", "application/json")
w.Write(out)
}

View File

@ -1,5 +1,7 @@
package models
import "go-udemy-web-1/internal/forms"
// TemplateData holds data sent from handlers to templates
type TemplateData struct {
StringMap map[string]string
@ -10,4 +12,5 @@ type TemplateData struct {
Flash string
Warning string
Error string
Form *forms.Form
}

View File

@ -2,12 +2,14 @@ package render
import (
"bytes"
"go-udemy-web-1/pkg/config"
"go-udemy-web-1/pkg/models"
"go-udemy-web-1/internal/config"
"go-udemy-web-1/internal/models"
"html/template"
"log"
"net/http"
"path/filepath"
"github.com/justinas/nosurf"
)
var app *config.AppConfig
@ -18,12 +20,13 @@ func NewTemplates(a *config.AppConfig) {
}
// AddDefaultData adds default template data
func AddDefaultData(td *models.TemplateData) *models.TemplateData {
func AddDefaultData(td *models.TemplateData, r *http.Request) *models.TemplateData {
td.CSRFToken = nosurf.Token(r)
return td
}
// RenderTemplate renders a HTML template file
func RenderTemplate(w http.ResponseWriter, tmpl string, td *models.TemplateData) {
func RenderTemplate(w http.ResponseWriter, r *http.Request, tmpl string, td *models.TemplateData) {
var tc map[string]*template.Template
if app.UseCache {
// get the template cache from the app config
@ -42,7 +45,7 @@ func RenderTemplate(w http.ResponseWriter, tmpl string, td *models.TemplateData)
// written successfully
buf := new(bytes.Buffer)
td = AddDefaultData(td)
td = AddDefaultData(td, r)
err := t.Execute(buf, td)
if err != nil {

View File

@ -1,74 +0,0 @@
package handlers
import (
"go-udemy-web-1/pkg/config"
"go-udemy-web-1/pkg/models"
"go-udemy-web-1/pkg/render"
"net/http"
)
// Repo the repository used by the handlers
var Repo *Repository
// Repository is the repository type
type Repository struct {
App *config.AppConfig
}
// NewRepo creates a new repository
func NewRepo(a *config.AppConfig) *Repository {
return &Repository{
App: a,
}
}
// NewHandlers sets the repository for the handlers
func NewHandlers(r *Repository) {
Repo = r
}
// Home is the home page handler
func (m *Repository) Home(w http.ResponseWriter, r *http.Request) {
remoteIP := r.RemoteAddr
m.App.Session.Put(r.Context(), "remote_ip", remoteIP)
render.RenderTemplate(w, "home.page.tmpl", &models.TemplateData{})
}
// About is the about page handler
func (m *Repository) About(w http.ResponseWriter, r *http.Request) {
// perform some logic
stringMap := make(map[string]string)
stringMap["test"] = "Hello world!"
remoteIP := m.App.Session.GetString(r.Context(), "remote_ip")
stringMap["remote_ip"] = remoteIP
// send the data to the template
render.RenderTemplate(w, "about.page.tmpl", &models.TemplateData{StringMap: stringMap})
}
// Contact is the contact page handler
func (m *Repository) Contact(w http.ResponseWriter, r *http.Request) {
render.RenderTemplate(w, "contact.page.tmpl", &models.TemplateData{})
}
// Generals is the General's Quarters page handler
func (m *Repository) Generals(w http.ResponseWriter, r *http.Request) {
render.RenderTemplate(w, "generals.page.tmpl", &models.TemplateData{})
}
// Majors is the Major's Suite page handler
func (m *Repository) Majors(w http.ResponseWriter, r *http.Request) {
render.RenderTemplate(w, "majors.page.tmpl", &models.TemplateData{})
}
// MakeReservation is the make reservation page handler
func (m *Repository) MakeReservation(w http.ResponseWriter, r *http.Request) {
render.RenderTemplate(w, "make-reservation.page.tmpl", &models.TemplateData{})
}
// MakeReservation is the make reservation page handler
func (m *Repository) Reservation(w http.ResponseWriter, r *http.Request) {
render.RenderTemplate(w, "reservation.page.tmpl", &models.TemplateData{})
}

View File

@ -7,7 +7,8 @@
<div class="col-md-6">
<h1 class="text-center mt-5">Search for Availability</h1>
<form action="reservation.html" method="get" novalidate class="needs-validation">
<form action="/availability" method="post" novalidate class="needs-validation">
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<div id="reservation-dates" class="row">
<div class="col mb-3">
<input required type="text" class="form-control" name="start" placeholder="Arrival">

View File

@ -45,7 +45,7 @@
</ul>
</li>
<li class="nav-item">
<a class="nav-link" href="/reservation">Book Now</a>
<a class="nav-link" href="/availability">Book Now</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/contact">Contact</a>
@ -177,25 +177,21 @@
title = "",
} = c;
const {value: formValues} = await Swal.fire({
const {value: result} = await Swal.fire({
title: title,
html: msg,
backdrop: false,
focusConfirm: false,
showCancelButton: true,
willOpen: () => {
console.log("test")
const elem = document.getElementById('reservation-dates-modal')
const rp = new DateRangePicker(elem, {
"format": "yyyy-mm-dd",
showOnFocus: true,
});
if (c.willOpen !== undefined) {
c.willOpen();
}
},
didOpen: () => {
return [
document.getElementById('start').removeAttribute("disabled"),
document.getElementById('end').removeAttribute("disabled"),
]
if (c.didOpen !== undefined) {
c.didOpen();
}
},
preConfirm: () => {
return [
@ -205,8 +201,14 @@
},
})
if (formValues) {
Swal.fire(JSON.stringify(formValues))
if (c.callback !== undefined) {
if (result &&
result.dismiss !== Swal.DismissReason.cancel &&
result !== "") {
c.callback(result);
} else {
c.callback(false);
}
}
}
@ -218,27 +220,6 @@
}
}
// document.getElementById("dummy").addEventListener('click', () => {
// notify("This is a message", "success");
// // notifyModal("title", "text", "success", "confirm");
// // Prompt().toast({msg: "Prompt Test"})
// Prompt().success({msg: "Success!"})
// // Prompt().error({msg: "Ooops"})
//
// let html = `
// <form action="reservation.html" method="get" novalidate class="needs-validation">
// <div id="reservation-dates-modal" class="row">
// <div class="col mb-3">
// <input disabled required type="text" class="form-control" name="start" id="start" placeholder="Arrival">
// </div>
// <div class="col mb-3">
// <input disabled required type="text" class="form-control" name="end" id="end" placeholder="Departure">
// </div>
// </div>
// </form>
// `;
// Prompt().custom({title: "Choose your dates", msg: html})
// });
</script>
{{block "js" .}}

View File

@ -23,8 +23,60 @@
<div class="row">
<div class="d-flex justify-content-center py-3">
<a href="/make-reservation-gq" class="btn btn-success">Check Availability</a>
<button id="check-availability" class="btn btn-success">Check Availability</button>
</div>
</div>
</div>
{{end}}
{{define "js"}}
<script>
document.getElementById("check-availability").addEventListener('click', () => {
let html = `
<form id="check-availability-form" action="/availability-json" method="post" novalidate class="needs-validation">
<div id="reservation-dates-modal" class="row">
<div class="col mb-3">
<input disabled required type="text" class="form-control" name="start" id="start" placeholder="Arrival">
</div>
<div class="col mb-3">
<input disabled required type="text" class="form-control" name="end" id="end" placeholder="Departure">
</div>
</div>
</form>
`;
Prompt().custom({
title: "Choose your dates",
msg: html,
willOpen: () => {
const elem = document.getElementById('reservation-dates-modal')
const rp = new DateRangePicker(elem, {
"format": "yyyy-mm-dd",
showOnFocus: true,
});
},
didOpen: () => {
return [
document.getElementById('start').removeAttribute("disabled"),
document.getElementById('end').removeAttribute("disabled"),
]
},
callback: (result) => {
console.log(result);
const formElem = document.getElementById("check-availability-form");
let formData = new FormData(formElem);
formData.append("csrf_token", "{{.CSRFToken}}");
fetch('/availability-json', {
method: "post",
body: formData,
})
.then(response => response.json())
.then(data => {
console.log(data);
})
},
});
});
</script>
{{end}}

View File

@ -23,8 +23,27 @@
<div class="row">
<div class="d-flex justify-content-center py-3">
<a href="/make-reservation-ms" class="btn btn-success">Check Availability</a>
<button id="check-availability" class="btn btn-success">Check Availability</button>
</div>
</div>
</div>
{{end}}
{{define "js"}}
<script>
document.getElementById("check-availability").addEventListener('click', () => {
let html = `
<form action="reservation.html" method="get" novalidate class="needs-validation">
<div id="reservation-dates-modal" class="row">
<div class="col mb-3">
<input disabled required type="text" class="form-control" name="start" id="start" placeholder="Arrival">
</div>
<div class="col mb-3">
<input disabled required type="text" class="form-control" name="end" id="end" placeholder="Departure">
</div>
</div>
</form>
`;
Prompt().custom({title: "Choose your dates", msg: html})
});
</script>
{{end}}

View File

@ -5,7 +5,9 @@
<div class="col">
<h1 class="text-center mt-3">Make reservation</h1>
<form method="post" action="" class="needs-validation" novalidate>
<!-- <form method="post" action="" class="needs-validation" novalidate> -->
<form method="post" action="" class="" novalidate>
<input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
<div class="form-group mt-5">
<label for="first_name">First name:</label>
<input type="text" name="first_name" id="first_name" class="form-control" required