Commit 32cb554d authored by Lee Brown's avatar Lee Brown

completed flash messages

parent 0344473c
package handlers
import (
"context"
"net/http"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"github.com/gorilla/schema"
"github.com/pkg/errors"
)
// Example represents the example pages
type Examples struct {
Renderer web.Renderer
}
// FlashMessages provides examples for displaying flash messages.
func (h *Examples) FlashMessages(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
// Display example messages only when we aren't handling an example form post.
if r.Method == http.MethodGet {
// Example displaying a success message.
webcontext.SessionFlashSuccess(ctx,
"Action Successful",
"You have reached an epic milestone.",
"800 hours", "304,232 lines of code")
// Example displaying an info message.
webcontext.SessionFlashInfo(ctx,
"Take the Tour",
"Learn more about the platform...",
"The pretty little trees in the forest.", "The happy little clouds in the sky.")
// Example displaying a warning message.
webcontext.SessionFlashWarning(ctx,
"Approaching Limit",
"Your account is reaching is limit, apply now!",
"Only overt benefit..")
// Example displaying an error message.
webcontext.SessionFlashError(ctx,
"Custom Error",
"SOMETIMES ITS HELPFUL TO SHOUT.",
"Listen to me.", "Leaders don't follow.")
// Example displaying a validation error which will use the json tag as the field name.
type valDemo struct {
Email string `json:"email_field_name" validate:"required,email"`
}
valErr := webcontext.Validator().StructCtx(ctx, valDemo{})
weberror.SessionFlashError(ctx, valErr)
// Generic error message for examples.
er := errors.New("Root causing undermined. Bailing out.")
// Example displaying a flash message for a web error with a message.
webErrWithMsg := weberror.WithMessage(ctx, er, "weberror:WithMessage")
weberror.SessionFlashError(ctx, webErrWithMsg)
// Example displaying a flash message for a web error.
webErr := weberror.NewError(ctx, er, http.StatusInternalServerError)
weberror.SessionFlashError(ctx, webErr)
// Example displaying a flash message for an error with a message.
erWithMsg := errors.WithMessage(er, "pkg/errors:WithMessage")
weberror.SessionFlashError(ctx, erWithMsg)
// Example displaying a flash message for an error that has been wrapped.
erWrap := errors.Wrap(er, "pkg/errors:Wrap")
weberror.SessionFlashError(ctx, erWrap)
}
data := make(map[string]interface{})
// Example displaying a validation error which will use the json tag as the field name.
{
type inlineDemo struct {
Email string `json:"email" validate:"required,email"`
HiddenField string `json:"hidden_field" validate:"required"`
}
req := new(inlineDemo)
f := func() error {
if r.Method == http.MethodPost {
err := r.ParseForm()
if err != nil {
return err
}
decoder := schema.NewDecoder()
if err := decoder.Decode(req, r.PostForm); err != nil {
return err
}
if err := webcontext.Validator().Struct(req); err != nil {
if ne, ok := weberror.NewValidationError(ctx, err); ok {
data["validationErrors"] = ne.(*weberror.Error)
return nil
} else {
return err
}
}
}
return nil
}
if err := f(); err != nil {
return web.RenderError(ctx, w, r, err, h.Renderer, tmplLayoutBase, tmplContentErrorGeneric, web.MIMETextHTMLCharsetUTF8)
}
data["form"] = req
if verr, ok := weberror.NewValidationError(ctx, webcontext.Validator().Struct(inlineDemo{})); ok {
data["validationDefaults"] = verr.(*weberror.Error)
}
}
return h.Renderer.Render(ctx, w, r, tmplLayoutBase, "examples-flash-messages.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
}
// Images provides examples for responsive images that are auto re-sized.
func (h *Examples) Images(ctx context.Context, w http.ResponseWriter, r *http.Request, params map[string]string) error {
// List of image sizes that will be used to resize the source image into. The resulting images will then be included
// as apart of the image src tag for a responsive image tag.
data := map[string]interface{}{
"imgSizes": []int{100, 200, 300, 400, 500},
}
return h.Renderer.Render(ctx, w, r, tmplLayoutBase, "examples-images.gohtml", web.MIMETextHTMLCharsetUTF8, http.StatusOK, data)
}
......@@ -3,13 +3,13 @@ package handlers
import (
"context"
"fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/notify"
"log"
"net/http"
"os"
"geeks-accelerator/oss/saas-starter-kit/internal/mid"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/notify"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
......@@ -74,6 +74,15 @@ func APP(shutdown chan os.Signal, log *log.Logger, env webcontext.Env, staticDir
app.Handle("POST", "/signup", s.Step1)
app.Handle("GET", "/signup", s.Step1)
// Register example endpoints.
ex := Examples{
Renderer: renderer,
}
// This route is not authenticated
app.Handle("POST", "/examples/flash-messages", ex.FlashMessages)
app.Handle("GET", "/examples/flash-messages", ex.FlashMessages)
app.Handle("GET", "/examples/images", ex.Images)
// Register geo
g := Geo{
MasterDB: masterDB,
......
......@@ -79,6 +79,11 @@ func (h *Signup) Step1(ctx context.Context, w http.ResponseWriter, r *http.Reque
return err
}
// Display a welcome message to the user.
webcontext.SessionFlashSuccess(ctx,
"Thank you for Joining",
"You workflow will be a breeze starting today.")
// Redirect the user to the dashboard.
http.Redirect(w, r, "/", http.StatusFound)
return nil
......
......@@ -2,6 +2,7 @@ package handlers
import (
"context"
"fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/notify"
project_routes "geeks-accelerator/oss/saas-starter-kit/internal/project-routes"
"net/http"
......@@ -206,7 +207,11 @@ func (h *User) ResetPassword(ctx context.Context, w http.ResponseWriter, r *http
}
}
// Display a flash message!!!
// Display a success message to the user to check their email.
webcontext.SessionFlashSuccess(ctx,
"Check your email",
fmt.Sprintf("An email was sent to '%s'. Click on the link in the email to finish resetting your password.", req.Email))
}
return nil
......
{{define "title"}}Error {{ .statusCode }}{{end}}
{{define "title"}}Error {{ .StatusCode }}{{end}}
{{define "style"}}
{{end}}
......@@ -7,10 +7,10 @@
<!-- Error Text -->
<div class="text-center mt-5">
<div class="error mx-auto" data-text="{{ .statusCode }}">{{ .statusCode }}</div>
<p class="lead text-gray-800 mb-5">{{ .errorMessage }}</p>
{{ if .fullError }}
<p class="text-gray-500 mb-0">{{ .fullError }}</p>
<div class="error mx-auto" data-text="{{ .StatusCode }}">{{ .StatusCode }}</div>
<p class="lead text-gray-800 mb-5">{{ .Error }}</p>
{{ if .Details }}
<p class="text-gray-500 mb-0">{{ .Details }}</p>
{{ end }}
</div>
......
{{define "title"}}Example - Flash Messages{{end}}
{{define "style"}}
{{end}}
{{define "content"}}
<h3>Inline Validation Example</h3>
<p>Any field error that is not displayed inline will still be displayed as apart of the the validation at the top of the page.</p>
<form class="user" method="post" novalidate>
<div class="form-group">
<input type="email" class="form-control form-control-user {{ ValidationFieldClass $.validationErrors "Email" }}" name="Email" value="{{ $.form.Email }}" placeholder="Enter Email Address...">
{{template "invalid-feedback" dict "validationDefaults" $.validationDefaults "validationErrors" $.validationErrors "fieldName" "Email" }}
</div>
<button class="btn btn-primary btn-user btn-block">
Submit Form
</button>
<hr>
</form>
{{end}}
{{define "js"}}
{{end}}
{{define "title"}}Example - Responsive Images{{end}}
{{define "style"}}
{{end}}
{{define "content"}}
<p>S3ImgSrcLarge
<img {{ S3ImgSrcLarge $._ctx "/assets/images/glacier-example-pic.jpg" }}/>
</p>
<p>S3ImgThumbSrcLarge
<img {{ S3ImgThumbSrcLarge $._ctx "/assets/images/glacier-example-pic.jpg" }}/>
</p>
<p>S3ImgSrcMedium
<img {{ S3ImgSrcMedium $._ctx "/assets/images/glacier-example-pic.jpg" }}/>
</p>
<p>S3ImgThumbSrcMedium
<img {{ S3ImgThumbSrcMedium $._ctx "/assets/images/glacier-example-pic.jpg" }}/>
</p>
<p>S3ImgSrcSmall
<img {{ S3ImgSrcSmall $._ctx "/assets/images/glacier-example-pic.jpg" }}/>
</p>
<p>S3ImgThumbSrcSmall
<img {{ S3ImgThumbSrcSmall $._ctx "/assets/images/glacier-example-pic.jpg" }}/>
</p>
<p>S3ImgSrc
<img {{ S3ImgSrc $._ctx "/assets/images/glacier-example-pic.jpg" $.imgSizes }}/>
</p>
<p>S3ImgUrl
<img src="{{ S3ImgUrl $._ctx "/assets/images/glacier-example-pic.jpg" 200 }}" />
</p>
{{end}}
{{define "js"}}
{{end}}
......@@ -12,11 +12,12 @@
<div class="col-lg-5 d-none d-lg-block bg-register-image"></div>
<div class="col-lg-7">
<div class="p-5">
{{ template "app-flashes" . }}
<div class="text-center">
<h1 class="h4 text-gray-900 mb-4">Create an Account!</h1>
</div>
{{ template "top-error" . }}
{{ template "validation-error" . }}
<hr>
......
......@@ -17,11 +17,12 @@
<div class="col-lg-6 d-none d-lg-block bg-login-image"></div>
<div class="col-lg-6">
<div class="p-5">
{{ template "app-flashes" . }}
<div class="text-center">
<h1 class="h4 text-gray-900 mb-4">Welcome Back!</h1>
</div>
{{ template "top-error" . }}
{{ template "validation-error" . }}
<form class="user" method="post" novalidate>
......
......@@ -17,12 +17,13 @@
<div class="col-lg-6 d-none d-lg-block bg-login-image"></div>
<div class="col-lg-6">
<div class="p-5">
{{ template "app-flashes" . }}
<div class="text-center">
<h1 class="h4 text-gray-900 mb-2">Reset Your Password</h1>
<p class="mb-4">.....</p>
</div>
{{ template "top-error" . }}
{{ template "validation-error" . }}
<form class="user" method="post" novalidate>
......
......@@ -17,12 +17,13 @@
<div class="col-lg-6 d-none d-lg-block bg-login-image"></div>
<div class="col-lg-6">
<div class="p-5">
{{ template "app-flashes" . }}
<div class="text-center">
<h1 class="h4 text-gray-900 mb-2">Forgot Your Password?</h1>
<p class="mb-4">We get it, stuff happens. Just enter your email address below and we'll send you a link to reset your password!</p>
</div>
{{ template "top-error" . }}
{{ template "validation-error" . }}
<form class="user" method="post" novalidate>
......
......@@ -107,7 +107,26 @@
{{ end }}
</div>
{{ end }}
{{ define "top-error" }}
{{ define "app-flashes" }}
{{ if .flashes }}
{{ range $f := .flashes }}
<div class="alert alert-{{ $f.Type }}" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"> <span aria-hidden="true">×</span> </button>
{{ if $f.Title }}<h3>{{ $f.Title }}</h3>{{end}}
{{ if $f.Text }}<p>{{ $f.Text }}</p>{{end}}
{{ if $f.Items }}
<ul>
{{ range $i := $f.Items }}
<li>{{ $i }}</li>
{{end}}
</ul>
{{ end }}
{{ if $f.Details }}
<p><small>{{ $f.Details }}</small></p>
{{ end }}
</div>
{{ end }}
{{ end }}
{{ if .error }}
{{ $errMsg := (ErrorMessage $._Ctx .error) }}
{{ $errDetails := (ErrorDetails $._Ctx .error) }}
......
......@@ -26,7 +26,7 @@
<!-- ============================================================== -->
<div class="container-fluid" id="page-content">
{{ template "top-error" . }}
{{ template "app-flashes" . }}
{{ template "validation-error" . }}
{{ template "content" . }}
......
......@@ -10,47 +10,73 @@
<div class="sidebar-brand-text mx-3">Example Project</div>
</a>
<!-- Divider -->
<hr class="sidebar-divider my-0">
{{ if HasAuth $._Ctx }}
<!-- Nav Item - Dashboard -->
<li class="nav-item">
<a class="nav-link" href="/">
<i class="fas fa-fw fa-tachometer-alt"></i>
<span>Dashboard</span></a>
</li>
<!-- Divider -->
<hr class="sidebar-divider my-0">
<!-- Nav Item - Dashboard -->
<li class="nav-item">
<a class="nav-link" href="/">
<i class="fas fa-fw fa-tachometer-alt"></i>
<span>Dashboard</span></a>
</li>
<!-- Divider -->
<hr class="sidebar-divider">
<!-- Heading -->
<div class="sidebar-heading">
Interface
</div>
<!-- Nav Item - Pages Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#navSectionProjects" aria-expanded="true" aria-controls="navSectionProjects">
<i class="fas fa-fw fa-cog"></i>
<span>Projects</span>
</a>
<div id="navSectionProjects" class="collapse" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<a class="collapse-item" href="buttons.html">Buttons</a>
<a class="collapse-item" href="cards.html">Cards</a>
</div>
</div>
</li>
<!-- Nav Item - Utilities Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#navSectionUsers" aria-expanded="true" aria-controls="navSectionUsers">
<i class="fas fa-fw fa-wrench"></i>
<span>Users</span>
</a>
<div id="navSectionUsers" class="collapse" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<a class="collapse-item" href="/users">Users</a>
</div>
</div>
</li>
{{ end }}
<!-- Divider -->
<hr class="sidebar-divider">
<!-- Heading -->
<div class="sidebar-heading">
Interface
Examples
</div>
<!-- Nav Item - Pages Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#navSectionProjects" aria-expanded="true" aria-controls="navSectionProjects">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#navSectionComponents" aria-expanded="true" aria-controls="navSectionComponents">
<i class="fas fa-fw fa-cog"></i>
<span>Projects</span>
</a>
<div id="navSectionProjects" class="collapse" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<a class="collapse-item" href="buttons.html">Buttons</a>
<a class="collapse-item" href="cards.html">Cards</a>
</div>
</div>
</li>
<!-- Nav Item - Utilities Collapse Menu -->
<li class="nav-item">
<a class="nav-link collapsed" href="#" data-toggle="collapse" data-target="#navSectionUsers" aria-expanded="true" aria-controls="navSectionUsers">
<i class="fas fa-fw fa-wrench"></i>
<span>Users</span>
<span>Components</span>
</a>
<div id="navSectionUsers" class="collapse" data-parent="#accordionSidebar">
<div id="navSectionComponents" class="collapse" aria-labelledby="headingTwo" data-parent="#accordionSidebar">
<div class="bg-white py-2 collapse-inner rounded">
<a class="collapse-item" href="/users">Users</a>
<h6 class="collapse-header">Custom Components:</h6>
<a class="collapse-item" href="/examples/flash-messages">Flash Messages</a>
<a class="collapse-item" href="/examples/images">Responsive Images</a>
</div>
</div>
</li>
......
This diff is collapsed.
......@@ -4,14 +4,13 @@ import (
"context"
"encoding/json"
"fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"html/template"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
)
......@@ -50,7 +49,7 @@ func RespondJsonError(ctx context.Context, w http.ResponseWriter, er error) erro
v.StatusCode = webErr.Status
return RespondJson(ctx, w, webErr.Display(ctx), webErr.Status)
return RespondJson(ctx, w, webErr.Response(ctx, false), webErr.Status)
}
// RespondJson converts a Go value to JSON and sends it to the client.
......@@ -120,7 +119,7 @@ func RespondErrorStatus(ctx context.Context, w http.ResponseWriter, er error, st
webErr := weberror.NewError(ctx, er, v.StatusCode).(*weberror.Error)
v.StatusCode = webErr.Status
respErr := webErr.Display(ctx).String()
respErr := webErr.Response(ctx, false).String()
switch webcontext.ContextEnv(ctx) {
case webcontext.Env_Dev, webcontext.Env_Stage:
......@@ -182,30 +181,17 @@ func RenderError(ctx context.Context, w http.ResponseWriter, r *http.Request, er
// If the error was of the type *Error, the handler has
// a specific status code and error to return.
webErr := weberror.NewError(ctx, er, v.StatusCode).(*weberror.Error)
v.StatusCode = webErr.Status
respErr := webErr.Display(ctx)
var fullError string
switch webcontext.ContextEnv(ctx) {
case webcontext.Env_Dev, webcontext.Env_Stage:
if webErr.Cause != nil && webErr.Cause.Error() != webErr.Err.Error() {
fullError = fmt.Sprintf("\n%s\n%+v", webErr.Error(), webErr.Cause)
} else {
fullError = fmt.Sprintf("%+v", webErr.Err)
}
fullError = strings.Replace(fullError, "\n", "<br/>", -1)
}
webErr := weberror.NewError(ctx, er, v.StatusCode).(*weberror.Error).Response(ctx, true)
v.StatusCode = webErr.StatusCode
data := map[string]interface{}{
"statusCode": webErr.Status,
"errorMessage": respErr.Error,
"fullError": template.HTML(fullError),
"StatusCode": webErr.StatusCode,
"Error": webErr.Error,
"Details": webErr.Details,
"Fields": webErr.Fields,
}
return renderer.Render(ctx, w, r, templateLayoutName, templateContentName, contentType, webErr.Status, data)
return renderer.Render(ctx, w, r, templateLayoutName, templateContentName, contentType, webErr.StatusCode, data)
}
// Static registers a new route with path prefix to serve static files from the
......
......@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/auth"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/webcontext"
"geeks-accelerator/oss/saas-starter-kit/internal/platform/web/weberror"
"html/template"
"math"
......@@ -351,10 +352,23 @@ func (r *TemplateRenderer) Render(ctx context.Context, w http.ResponseWriter, re
}
}
// If there is a session, check for flashes and ensure the session is saved.
sess := webcontext.ContextSession(ctx)
if sess != nil {
// Load any flash messages and append to response data to be included in the rendered template.
if flashes := sess.Flashes(); len(flashes) > 0 {
renderData["flashes"] = flashes
}
// Save the session before writing to the response for the session cookie to be sent to the client.
if err := sess.Save(req, w); err != nil {
return err
}
}
// Render template with data.
err := t.Execute(w, renderData)
if err != nil {
return err
if err := t.Execute(w, renderData); err != nil {
return errors.WithStack(err)
}
return nil
......
package webcontext
import (
"context"
"encoding/gob"
"html/template"
)
type FlashType string
var (
FlashType_Success FlashType = "success"
FlashType_Info FlashType = "info"
FlashType_Warning FlashType = "warning"
FlashType_Error FlashType = "danger"
)
type FlashMsg struct {
Type FlashType `json:"type"`
Title string `json:"title"`
Text string `json:"text"`
Items []string `json:"items"`
Details string `json:"details"`
}
func (r FlashMsg) Response(ctx context.Context) map[string]interface{} {
var items []template.HTML
for _, i := range r.Items {
items = append(items, template.HTML(i))
}
return map[string]interface{}{
"Type": r.Type,
"Title": r.Title,
"Text": template.HTML(r.Text),
"Items": items,
"Details": template.HTML(r.Details),
}
}
func init() {
gob.Register(&FlashMsg{})
}
// SessionAddFlash loads the session from context that is provided by the session middleware and
// adds the message to the session. The renderer should save the session before writing the response
// to the client or save be directly invoked.
func SessionAddFlash(ctx context.Context, msg FlashMsg) {
ContextSession(ctx).AddFlash(msg.Response(ctx))
}
// SessionFlashSuccess add a message with type Success.
func SessionFlashSuccess(ctx context.Context, title, text string, items ...string) {
sessionFlashType(ctx, FlashType_Success, title, text, items...)
}
// SessionFlashInfo add a message with type Info.
func SessionFlashInfo(ctx context.Context, title, text string, items ...string) {
sessionFlashType(ctx, FlashType_Info, title, text, items...)
}
// SessionFlashWarning add a message with type Warning.
func SessionFlashWarning(ctx context.Context, title, text string, items ...string) {
sessionFlashType(ctx, FlashType_Warning, title, text, items...)
}