1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-18 10:07:02 +00:00

Many small refinements to front/backend UI

Changes:
 - Added a friendlyTime helper for nicer timestamps
 - Added validation to most action methods
 - Added error flash to several template files
 - Now making use to c.RenderError() to handle action errors
 - Removed message list slideUp() effect, takes too long
 - Fixed a problem with my vim indentation configuration, so CSS and
   HTML should be indented more consistently.
This commit is contained in:
James Hillyerd
2012-10-14 16:02:14 -07:00
parent 264d1d0165
commit da8e0bc508
7 changed files with 279 additions and 218 deletions

View File

@@ -0,0 +1,19 @@
package controllers
import (
"github.com/robfig/revel"
"html/template"
"time"
)
func init() {
rev.TRACE.Println("Registering helpers")
rev.Funcs["friendlyTime"] = func(t time.Time) template.HTML {
ty, tm, td := t.Date()
ny, nm, nd := time.Now().Date()
if (ty == ny) && (tm == nm) && (td == nd) {
return template.HTML(t.Format("03:04:05 PM"))
}
return template.HTML(t.Format("Mon Jan 2, 2006"))
}
}

View File

@@ -10,22 +10,34 @@ type Mailbox struct {
} }
func (c Mailbox) Index(name string) rev.Result { func (c Mailbox) Index(name string) rev.Result {
c.Validation.Required(name).Message("Account name is required")
if c.Validation.HasErrors() {
c.Validation.Keep()
c.FlashParams()
return c.Redirect(Application.Index)
}
return c.Render(name) return c.Render(name)
} }
func (c Mailbox) List(name string) rev.Result { func (c Mailbox) List(name string) rev.Result {
c.Validation.Required(name).Message("Account name is required")
if c.Validation.HasErrors() {
c.Validation.Keep()
c.FlashParams()
return c.Redirect(Application.Index)
}
ds := inbucket.NewDataStore() ds := inbucket.NewDataStore()
mb, err := ds.MailboxFor(name) mb, err := ds.MailboxFor(name)
if err != nil { if err != nil {
rev.ERROR.Printf(err.Error()) return c.RenderError(err)
c.Flash.Error(err.Error())
return c.Redirect(Application.Index)
} }
messages, err := mb.GetMessages() messages, err := mb.GetMessages()
if err != nil { if err != nil {
rev.ERROR.Printf(err.Error()) return c.RenderError(err)
c.Flash.Error(err.Error())
return c.Redirect(Application.Index)
} }
rev.INFO.Printf("Got %v messsages", len(messages)) rev.INFO.Printf("Got %v messsages", len(messages))
@@ -34,24 +46,27 @@ func (c Mailbox) List(name string) rev.Result {
} }
func (c Mailbox) Show(name string, id string) rev.Result { func (c Mailbox) Show(name string, id string) rev.Result {
c.Validation.Required(name).Message("Account name is required")
c.Validation.Required(id).Message("Message ID is required")
if c.Validation.HasErrors() {
c.Validation.Keep()
c.FlashParams()
return c.Redirect(Application.Index)
}
ds := inbucket.NewDataStore() ds := inbucket.NewDataStore()
mb, err := ds.MailboxFor(name) mb, err := ds.MailboxFor(name)
if err != nil { if err != nil {
rev.ERROR.Printf(err.Error()) return c.RenderError(err)
c.Flash.Error(err.Error())
return c.Redirect(Application.Index)
} }
message, err := mb.GetMessage(id) message, err := mb.GetMessage(id)
if err != nil { if err != nil {
rev.ERROR.Printf(err.Error()) return c.RenderError(err)
c.Flash.Error(err.Error())
return c.Redirect(Application.Index)
} }
_, body, err := message.ReadBody() _, body, err := message.ReadBody()
if err != nil { if err != nil {
rev.ERROR.Printf(err.Error()) return c.RenderError(err)
c.Flash.Error(err.Error())
return c.Redirect(Application.Index)
} }
c.Response.Out.Header().Set("Expires", "-1") c.Response.Out.Header().Set("Expires", "-1")
@@ -59,47 +74,53 @@ func (c Mailbox) Show(name string, id string) rev.Result {
} }
func (c Mailbox) Delete(name string, id string) rev.Result { func (c Mailbox) Delete(name string, id string) rev.Result {
c.Validation.Required(name).Message("Account name is required")
c.Validation.Required(id).Message("Message ID is required")
if c.Validation.HasErrors() {
c.Validation.Keep()
c.FlashParams()
return c.Redirect(Application.Index)
}
ds := inbucket.NewDataStore() ds := inbucket.NewDataStore()
mb, err := ds.MailboxFor(name) mb, err := ds.MailboxFor(name)
if err != nil { if err != nil {
rev.ERROR.Printf(err.Error()) return c.RenderError(err)
c.Flash.Error(err.Error())
return c.Redirect(Application.Index)
} }
message, err := mb.GetMessage(id) message, err := mb.GetMessage(id)
if err != nil { if err != nil {
rev.ERROR.Printf(err.Error()) return c.RenderError(err)
c.Flash.Error(err.Error())
return c.Redirect(Application.Index)
} }
err = message.Delete() err = message.Delete()
if err != nil { if err != nil {
rev.ERROR.Printf(err.Error()) return c.RenderError(err)
c.Flash.Error(err.Error())
return c.Redirect(Application.Index)
} }
return c.RenderText("OK") return c.RenderText("OK")
} }
func (c Mailbox) Source(name string, id string) rev.Result { func (c Mailbox) Source(name string, id string) rev.Result {
c.Validation.Required(name).Message("Account name is required")
c.Validation.Required(id).Message("Message ID is required")
if c.Validation.HasErrors() {
c.Validation.Keep()
c.FlashParams()
return c.Redirect(Application.Index)
}
ds := inbucket.NewDataStore() ds := inbucket.NewDataStore()
mb, err := ds.MailboxFor(name) mb, err := ds.MailboxFor(name)
if err != nil { if err != nil {
rev.ERROR.Printf(err.Error()) return c.RenderError(err)
c.Flash.Error(err.Error())
return c.Redirect(Application.Index)
} }
message, err := mb.GetMessage(id) message, err := mb.GetMessage(id)
if err != nil { if err != nil {
rev.ERROR.Printf(err.Error()) return c.RenderError(err)
c.Flash.Error(err.Error())
return c.Redirect(Application.Index)
} }
raw, err := message.ReadRaw() raw, err := message.ReadRaw()
if err != nil { if err != nil {
rev.ERROR.Printf(err.Error()) return c.RenderError(err)
c.Flash.Error(err.Error())
return c.Redirect(Application.Index)
} }
c.Response.Out.Header().Set("Expires", "-1") c.Response.Out.Header().Set("Expires", "-1")

View File

@@ -3,6 +3,8 @@
{{template "menu.html" .}} {{template "menu.html" .}}
<div id="colTwo"> <div id="colTwo">
{{template "errors.html" .}}
<p>Inbucket is an email testing service; it will accept email for any email <p>Inbucket is an email testing service; it will accept email for any email
address and make it available to view without a password.</p> address and make it available to view without a password.</p>
<p>To view email for a particular address, enter the username portion <p>To view email for a particular address, enter the username portion

View File

@@ -3,54 +3,54 @@
{{template "header.html" .}} {{template "header.html" .}}
{{$name := .name}} {{$name := .name}}
<script> <script>
function listLoaded() { function listLoaded() {
$('.listEntry').hover( $('.listEntry').hover(
function() { function() {
$(this).addClass("listEntryHover") $(this).addClass("listEntryHover")
}, },
function() { function() {
$(this).removeClass("listEntryHover") $(this).removeClass("listEntryHover")
} }
).click( ).click(
function() { function() {
$('.listEntry').removeClass("listEntrySelected") $('.listEntry').removeClass("listEntrySelected")
$(this).addClass("listEntrySelected") $(this).addClass("listEntrySelected")
$('#emailContent').load('/mailbox/show/{{$name}}/' + this.id) $('#emailContent').load('/mailbox/show/{{$name}}/' + this.id)
} }
) )
$("#messageList").slideDown() $("#messageList").slideDown()
} }
function loadList() { function loadList() {
$('#messageList').load("/mailbox/list/{{$name}}", listLoaded) $('#messageList').load("/mailbox/list/{{$name}}", listLoaded)
} }
function reloadList() {
$('#messageList').hide()
loadList()
}
function listInit() { function reloadList() {
$("#messageList").hide() $('#messageList').hide()
loadList() loadList()
} }
function deleteMessage(id) {
$('#emailContent').empty()
$.ajax({
type: 'POST',
url: '/mailbox/delete/{{$name}}/' + id,
success: reloadList
})
}
function messageSource(id) { function listInit() {
window.open('/mailbox/source/{{$name}}/' + id, '_blank', $("#messageList").hide()
'width=800,height=600,' + loadList()
'menubar=no,resizable=yes,scrollbars=yes,status=no,toolbar=no') }
}
$(document).ready(listInit) function deleteMessage(id) {
$('#emailContent').empty()
$.ajax({
type: 'POST',
url: '/mailbox/delete/{{$name}}/' + id,
success: reloadList
})
}
function messageSource(id) {
window.open('/mailbox/source/{{$name}}/' + id, '_blank',
'width=800,height=600,' +
'menubar=no,resizable=yes,scrollbars=yes,status=no,toolbar=no')
}
$(document).ready(listInit)
</script> </script>
<div id="colOne"> <div id="colOne">
@@ -59,12 +59,14 @@
<h2>mail for {{.name}}</h2> <h2>mail for {{.name}}</h2>
</div> </div>
<div class="box" style="text-align:center; padding-bottom:10px;"> <div class="box" style="text-align:center; padding-bottom:10px;">
<a href="javascript:reloadList()">Refresh List</a> <a href="javascript:reloadList()">Refresh List</a>
</div> </div>
<div id="messageList"></div> <div id="messageList"></div>
</div> </div>
<div id="colTwo"> <div id="colTwo">
{{template "errors.html" .}}
<div id="emailContent"> <div id="emailContent">
<p>Select a message at left, or enter a different username into the box on upper right.</p> <p>Select a message at left, or enter a different username into the box on upper right.</p>
</div> </div>

View File

@@ -1,9 +1,9 @@
{{$name := .name}} {{$name := .name}}
{{range .messages}} {{range .messages}}
<div class="box listEntry" id="{{.Id}}"> <div class="box listEntry" id="{{.Id}}">
<div class="subject">{{/*<a href="/mailbox/show/{{$name}}/{{.Id}}">*/}}{{.Subject}}</div> <div class="subject">{{.Subject}}</div>
<div class="from">{{.From}}</div> <div class="from">{{.From}}</div>
<div class="date">{{.Date}}</div> <div class="date">{{friendlyTime .Date}}</div>
</div> </div>
{{else}} {{else}}
<div class="box"> <div class="box">

10
app/views/errors.html Normal file
View File

@@ -0,0 +1,10 @@
{{if .errors}}
<div class="errors">
<p>Please fix the following errors and resubmit:<p>
<ul>
{{range .errors}}
<li>{{.Message}}</li>
{{end}}
</ul>
</div>
{{end}}

View File

@@ -2,305 +2,312 @@
Design by Free CSS Templates Design by Free CSS Templates
http://www.freecsstemplates.org http://www.freecsstemplates.org
Released for free under a Creative Commons Attribution 2.5 License Released for free under a Creative Commons Attribution 2.5 License
*/ */
/* Elements */ /* Elements */
body { body {
margin: 20px 0; margin: 20px 0;
font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; font-family: "Trebuchet MS", Arial, Helvetica, sans-serif;
font-size: 12px; font-size: 12px;
color: #808080; color: #808080;
} }
h1 { h1 {
} }
h2, h3, h4, h5, h6 { h2, h3, h4, h5, h6 {
color: #2582A4; color: #2582A4;
} }
p, ol, ul { p, ol, ul {
text-align: justify; text-align: justify;
} }
strong { strong {
font-weight: bold; font-weight: bold;
color: #808080; color: #808080;
} }
a { a {
color: #87A019; color: #87A019;
} }
a:hover { a:hover {
text-decoration: none; text-decoration: none;
color: #2582A4; color: #2582A4;
} }
.box { .box {
margin-bottom: 0; margin-bottom: 0;
padding-bottom: 0; padding-bottom: 0;
background: url(/public/images/img07.gif) repeat-x left bottom; background: url(/public/images/img07.gif) repeat-x left bottom;
} }
.bottom { .bottom {
display: block; display: block;
margin-bottom: 0; margin-bottom: 0;
padding-bottom: 10px; padding-bottom: 10px;
background: url(/public/images/img08.gif) no-repeat left bottom; background: url(/public/images/img08.gif) no-repeat left bottom;
} }
.image { .image {
float: left; float: left;
margin: 3px 0 0 0; margin: 3px 0 0 0;
padding: 0; padding: 0;
} }
.indent { .indent {
margin-left: 125px; margin-left: 125px;
} }
/* Header */ /* Header */
#header { #header {
width: 928px; width: 928px;
height: 37px; height: 37px;
margin: 0 auto; margin: 0 auto;
background: #BECF74 url(/public/images/img01.gif) repeat-x; background: #BECF74 url(/public/images/img01.gif) repeat-x;
border-bottom: 3px solid #FFFFFF; border-bottom: 3px solid #FFFFFF;
} }
/* Menu */ /* Menu */
#menu { #menu {
float: left; float: left;
width: 600px; width: 600px;
height: 37px; height: 37px;
margin: 0; margin: 0;
padding: 0 0 0 10px; padding: 0 0 0 10px;
list-style: none; list-style: none;
background: url(/public/images/img02.gif) no-repeat; background: url(/public/images/img02.gif) no-repeat;
} }
#menu li { #menu li {
display: inline; display: inline;
} }
#menu a { #menu a {
display: block; display: block;
float: left; float: left;
height: 27px; height: 27px;
padding: 10px 15px 0px 15px; padding: 10px 15px 0px 15px;
text-transform: lowercase; text-transform: lowercase;
text-decoration: none; text-decoration: none;
font-size: 12px; font-size: 12px;
font-weight: bold; font-weight: bold;
color: #FFFFFF; color: #FFFFFF;
} }
#menu a:hover { #menu a:hover {
background: #A4B74C url(/public/images/img04.gif) repeat-x; background: #A4B74C url(/public/images/img04.gif) repeat-x;
} }
/* Search */ /* Search */
#search { #search {
float: right; float: right;
height: 28px; height: 28px;
margin: 0; margin: 0;
padding: 9px 25px 0 0; padding: 9px 25px 0 0;
background: url(/public/images/img03.gif) no-repeat right top; background: url(/public/images/img03.gif) no-repeat right top;
} }
#search fieldset { #search fieldset {
display: inline; display: inline;
margin: 0; margin: 0;
padding: 0; padding: 0;
border: none; border: none;
} }
#input1 { #input1 {
width: 164px; width: 164px;
padding: 2px 5px; padding: 2px 5px;
background: #FFFFFF; background: #FFFFFF;
border: none; border: none;
} }
#input2 { #input2 {
height: 20px; height: 20px;
background: #87C4DB; background: #87C4DB;
border: none; border: none;
text-transform: lowercase; text-transform: lowercase;
font-size: 10px; font-size: 10px;
font-weight: bold; font-weight: bold;
color: #FFFFFF; color: #FFFFFF;
} }
/* Content */ /* Content */
#content { #content {
width: 928px; width: 928px;
margin: 0 auto; margin: 0 auto;
} }
#colOne { #colOne {
float: left; float: left;
width: 238px; width: 238px;
background: url(/public/images/img05.gif) repeat-x; background: url(/public/images/img05.gif) repeat-x;
} }
#colOne h3 { #colOne h3 {
padding: 0 20px; padding: 0 20px;
} }
#colOne ul { #colOne ul {
margin-left: 0; margin-left: 0;
padding-left: 20px; padding-left: 20px;
padding-right: 20px; padding-right: 20px;
list-style: none; list-style: none;
} }
#colOne li { #colOne li {
padding: 5px 0; padding: 5px 0;
border-top: 1px solid #EFEFEF; border-top: 1px solid #EFEFEF;
} }
#colOne li.first { #colOne li.first {
border: none; border: none;
} }
#colTwo { #colTwo {
float: right; float: right;
width: 617px; width: 617px;
padding: 30px 40px 0 30px; padding: 30px 40px 0 30px;
background: url(/public/images/img09.gif) repeat-x; background: url(/public/images/img09.gif) repeat-x;
} }
#colTwo h2 { #colTwo h2 {
margin-top: 0; margin-top: 0;
font-size: 26px; font-size: 26px;
} }
#colTwo h3 { #colTwo h3 {
margin-bottom: 0; margin-bottom: 0;
font-size: 18px; font-size: 18px;
} }
#colTwo h4 { #colTwo h4 {
margin-top: 0; margin-top: 0;
font-size: 11px; font-size: 11px;
font-weight: normal; font-weight: normal;
} }
#colTwo .box { #colTwo .box {
margin: 0 -30px 30px -20px; margin: 0 -30px 30px -20px;
padding: 0 30px 0 20px; padding: 0 30px 0 20px;
} }
#colTwo .bottom { #colTwo .bottom {
margin: 0 0 20px -20px; margin: 0 0 20px -20px;
padding: 0 0 20px 20px; padding: 0 0 20px 20px;
} }
/* Logo */ /* Logo */
#logo { #logo {
height: 150px; height: 150px;
background: url(/public/images/img06.gif) no-repeat; background: url(/public/images/img06.gif) no-repeat;
} }
#logo h1, #logo h2, #logo a { #logo h1, #logo h2, #logo a {
margin: 0; margin: 0;
padding: 0; padding: 0;
text-decoration: none; text-decoration: none;
text-transform: lowercase; text-transform: lowercase;
text-align: center; text-align: center;
color: #FFFFFF; color: #FFFFFF;
} }
#logo h1 { #logo h1 {
padding-top: 25px; padding-top: 25px;
font-size: 34px; font-size: 34px;
} }
#logo h2 { #logo h2 {
margin-top: -5px; margin-top: -5px;
font-size: 12px; font-size: 12px;
} }
/* Footer */ /* Footer */
#footer { #footer {
clear: both; clear: both;
width: 928px; width: 928px;
margin: 0 auto; margin: 0 auto;
padding-top: 40px; padding-top: 40px;
background: url(/public/images/img11.gif) repeat-x; background: url(/public/images/img11.gif) repeat-x;
} }
#footer p { #footer p {
margin: 0; margin: 0;
text-align: center; text-align: center;
} }
.listEntry { .listEntry {
color: #909090; color: #909090;
padding: 5px; padding: 5px;
} }
.listEntry > .subject { .listEntry > .subject {
color: #2582A4; color: #2582A4;
font-weight: bold; font-weight: bold;
} }
.listEntrySelected { .listEntrySelected {
background: #becf74; background: #becf74;
color: #666; color: #666;
} }
.listEntryHover { .listEntryHover {
background: #8ac6dc; background: #8ac6dc;
color: #666; color: #666;
} }
#emailContent { #emailContent {
padding-bottom: 20px; padding-bottom: 20px;
min-height: 300px; min-height: 300px;
} }
#emailHeader { #emailHeader {
border-collapse: collapse; border-collapse: collapse;
} }
#emailHeader th, #emailHeader td { #emailHeader th, #emailHeader td {
text-align: left; text-align: left;
padding: 0 3px 3px 0; padding: 0 3px 3px 0;
} }
#emailSubject { #emailSubject {
border-bottom: 1px #606060 solid; border-bottom: 1px #606060 solid;
margin: 0; margin: 0;
width: 617px; width: 617px;
} }
#emailBody { #emailBody {
color: #000000; color: #000000;
} }
#emailActions { #emailActions {
padding: 5px 0; padding: 5px 0;
margin: 0 0 10px 0; margin: 0 0 10px 0;
} }
#emailActions a { #emailActions a {
background: #8ac6dc; background: #8ac6dc;
color: #fff; color: #fff;
text-decoration: none; text-decoration: none;
font-weight: bold; font-weight: bold;
padding: 5px; padding: 5px;
} }
#emailActions a:hover { #emailActions a:hover {
background: #becf74; background: #becf74;
} }
.errors {
background-color: #ffa0a0;
color: #333;
padding: 5px 10px;
}