Contact Us, Feedback, and Reporting
* Add the Contact page where users can contact the site admins for feedback or to report a problematic user, photo or message. * Reports go into the admin Feedback table. * Admin nav bar indicates number of unread feedbacks. * Add "Report" button to profile pages, photo cards, and the top of Direct Message threads. Misc changes: * Send emails out asynchronously for more responsive page loads.
This commit is contained in:
parent
50fc93abfb
commit
e42cebe4b8
|
@ -64,4 +64,37 @@ var (
|
||||||
"interests",
|
"interests",
|
||||||
"music_movies",
|
"music_movies",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Choices for the Contact Us subject
|
||||||
|
ContactUsChoices = []ContactUs{
|
||||||
|
{
|
||||||
|
Header: "Website Feedback",
|
||||||
|
Options: []Option{
|
||||||
|
{"feedback", "Website feedback"},
|
||||||
|
{"feature", "Make a feature request"},
|
||||||
|
{"bug", "Report a bug or broken feature"},
|
||||||
|
{"other", "General/miscellaneous/other"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: "Report a Problem",
|
||||||
|
Options: []Option{
|
||||||
|
{"report.user", "Report a problematic user"},
|
||||||
|
{"report.photo", "Report a problematic photo"},
|
||||||
|
{"report.message", "Report a direct message conversation"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ContactUs choices for the subject drop-down.
|
||||||
|
type ContactUs struct {
|
||||||
|
Header string
|
||||||
|
Options []Option
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option for select boxes.
|
||||||
|
type Option struct {
|
||||||
|
Value string
|
||||||
|
Label string
|
||||||
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ var (
|
||||||
PageSizeFriends = 12
|
PageSizeFriends = 12
|
||||||
PageSizeBlockList = 12
|
PageSizeBlockList = 12
|
||||||
PageSizeAdminCertification = 20
|
PageSizeAdminCertification = 20
|
||||||
|
PageSizeAdminFeedback = 20
|
||||||
PageSizeSiteGallery = 18
|
PageSizeSiteGallery = 18
|
||||||
PageSizeUserGallery = 18
|
PageSizeUserGallery = 18
|
||||||
PageSizeInboxList = 20 // sidebar list
|
PageSizeInboxList = 20 // sidebar list
|
||||||
|
|
162
pkg/controller/admin/feedback.go
Normal file
162
pkg/controller/admin/feedback.go
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/config"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/models"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/photo"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/session"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Feedback controller (/admin/feedback)
|
||||||
|
func Feedback() http.HandlerFunc {
|
||||||
|
tmpl := templates.Must("admin/feedback.html")
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Query params.
|
||||||
|
var (
|
||||||
|
acknowledged = r.FormValue("acknowledged") == "true"
|
||||||
|
intent = r.FormValue("intent")
|
||||||
|
visit = r.FormValue("visit") == "true" // visit the linked table ID
|
||||||
|
profile = r.FormValue("profile") == "true" // visit associated user profile
|
||||||
|
verdict = r.FormValue("verdict")
|
||||||
|
fb *models.Feedback
|
||||||
|
)
|
||||||
|
|
||||||
|
currentUser, err := session.CurrentUser(r)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't get your current user: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Working on a target message?
|
||||||
|
if idStr := r.FormValue("id"); idStr != "" {
|
||||||
|
if idInt, err := strconv.Atoi(idStr); err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't parse id param: %s", err)
|
||||||
|
} else {
|
||||||
|
fb, err = models.GetFeedback(uint64(idInt))
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't load feedback message %d: %s", idInt, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Are we visiting a linked resource (via TableID)?
|
||||||
|
if fb != nil && fb.TableID > 0 && visit {
|
||||||
|
switch fb.TableName {
|
||||||
|
case "users":
|
||||||
|
user, err := models.GetUser(fb.TableID)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't visit user %d: %s", fb.TableID, err)
|
||||||
|
} else {
|
||||||
|
templates.Redirect(w, "/u/"+user.Username)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "photos":
|
||||||
|
pic, err := models.GetPhoto(fb.TableID)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't get photo %d: %s", fb.TableID, err)
|
||||||
|
} else {
|
||||||
|
// Going to the user's profile page?
|
||||||
|
if profile {
|
||||||
|
user, err := models.GetUser(pic.UserID)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't visit user %d: %s", fb.TableID, err)
|
||||||
|
} else {
|
||||||
|
templates.Redirect(w, "/u/"+user.Username)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Direct link to the photo.
|
||||||
|
templates.Redirect(w, photo.URLPath(pic.Filename))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case "messages":
|
||||||
|
// To read this message we will need to impersonate the reporter.
|
||||||
|
user, err := models.GetUser(fb.UserID)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't get reporting user ID %d: %s", fb.UserID, err)
|
||||||
|
} else {
|
||||||
|
if err := session.ImpersonateUser(w, r, user, currentUser); err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't impersonate user: %s", err)
|
||||||
|
} else {
|
||||||
|
// Redirect to the thread.
|
||||||
|
session.Flash(w, r, "NOTICE: You are now impersonating %s to view their inbox.", user.Username)
|
||||||
|
templates.Redirect(w, fmt.Sprintf("/messages/read/%d", fb.TableID))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
session.FlashError(w, r, "Couldn't visit TableID %s/%d: not a supported TableName", fb.TableName, fb.TableID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Are we (un)acknowledging a message?
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
if fb == nil {
|
||||||
|
session.FlashError(w, r, "Missing feedback ID for this POST!")
|
||||||
|
} else {
|
||||||
|
switch verdict {
|
||||||
|
case "acknowledge":
|
||||||
|
fb.Acknowledged = true
|
||||||
|
if err := fb.Save(); err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't save message: %s", err)
|
||||||
|
} else {
|
||||||
|
session.Flash(w, r, "Message acknowledged!")
|
||||||
|
}
|
||||||
|
case "unacknowledge":
|
||||||
|
fb.Acknowledged = false
|
||||||
|
if err := fb.Save(); err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't save message: %s", err)
|
||||||
|
} else {
|
||||||
|
session.Flash(w, r, "Message acknowledged!")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
session.FlashError(w, r, "Unsupported verdict: %s", verdict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
templates.Redirect(w, r.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the feedback.
|
||||||
|
pager := &models.Pagination{
|
||||||
|
Page: 1,
|
||||||
|
PerPage: config.PageSizeAdminFeedback,
|
||||||
|
Sort: "updated_at desc",
|
||||||
|
}
|
||||||
|
pager.ParsePage(r)
|
||||||
|
page, err := models.PaginateFeedback(acknowledged, intent, pager)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't load feedback from DB: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map user IDs.
|
||||||
|
var userIDs = []uint64{}
|
||||||
|
for _, p := range page {
|
||||||
|
if p.UserID > 0 {
|
||||||
|
userIDs = append(userIDs, p.UserID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
userMap, err := models.MapUsers(userIDs)
|
||||||
|
if err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't map user IDs: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var vars = map[string]interface{}{
|
||||||
|
"Intent": intent,
|
||||||
|
"Acknowledged": acknowledged,
|
||||||
|
"Feedback": page,
|
||||||
|
"UserMap": userMap,
|
||||||
|
"Pager": pager,
|
||||||
|
}
|
||||||
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -28,11 +28,14 @@ func Inbox() http.HandlerFunc {
|
||||||
var showSent = r.FormValue("box") == "sent"
|
var showSent = r.FormValue("box") == "sent"
|
||||||
|
|
||||||
// Are we reading a specific message?
|
// Are we reading a specific message?
|
||||||
var viewThread []*models.Message
|
var (
|
||||||
var threadPager *models.Pagination
|
viewThread []*models.Message
|
||||||
var composeToUsername string
|
threadPager *models.Pagination
|
||||||
|
composeToUsername string
|
||||||
|
msgId int
|
||||||
|
)
|
||||||
if uri := ReadURLRegexp.FindStringSubmatch(r.URL.Path); uri != nil {
|
if uri := ReadURLRegexp.FindStringSubmatch(r.URL.Path); uri != nil {
|
||||||
msgId, _ := strconv.Atoi(uri[1])
|
msgId, _ = strconv.Atoi(uri[1])
|
||||||
if msg, err := models.GetMessage(uint64(msgId)); err != nil {
|
if msg, err := models.GetMessage(uint64(msgId)); err != nil {
|
||||||
session.FlashError(w, r, "Message not found.")
|
session.FlashError(w, r, "Message not found.")
|
||||||
templates.Redirect(w, "/messages")
|
templates.Redirect(w, "/messages")
|
||||||
|
@ -127,6 +130,7 @@ func Inbox() http.HandlerFunc {
|
||||||
"ViewThread": viewThread, // nil on inbox page
|
"ViewThread": viewThread, // nil on inbox page
|
||||||
"ThreadPager": threadPager,
|
"ThreadPager": threadPager,
|
||||||
"ReplyTo": composeToUsername,
|
"ReplyTo": composeToUsername,
|
||||||
|
"MessageID": msgId,
|
||||||
}
|
}
|
||||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
|
165
pkg/controller/index/contact.go
Normal file
165
pkg/controller/index/contact.go
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
package index
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/config"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/log"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/mail"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/markdown"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/models"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/session"
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/templates"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Contact or report a problem.
|
||||||
|
func Contact() http.HandlerFunc {
|
||||||
|
tmpl := templates.Must("contact.html")
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Query and form POST parameters.
|
||||||
|
var (
|
||||||
|
intent = r.FormValue("intent")
|
||||||
|
subject = r.FormValue("subject")
|
||||||
|
title = "Contact Us"
|
||||||
|
message = r.FormValue("message")
|
||||||
|
replyTo = r.FormValue("email")
|
||||||
|
tableID int
|
||||||
|
tableName string
|
||||||
|
tableLabel string // front-end user feedback about selected report item
|
||||||
|
messageRequired = true // unless we have a table ID to work with
|
||||||
|
success = "Thank you for your feedback! Your message has been delivered to the website administrators."
|
||||||
|
)
|
||||||
|
|
||||||
|
// For report intents: ID of the user, photo, message, etc.
|
||||||
|
tableID, _ = strconv.Atoi(r.FormValue("id"))
|
||||||
|
if tableID > 0 {
|
||||||
|
messageRequired = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// In what context is the ID given?
|
||||||
|
if subject != "" && tableID > 0 {
|
||||||
|
switch subject {
|
||||||
|
case "report.user":
|
||||||
|
tableName = "users"
|
||||||
|
if user, err := models.GetUser(uint64(tableID)); err == nil {
|
||||||
|
tableLabel = fmt.Sprintf(`User account "%s"`, user.Username)
|
||||||
|
} else {
|
||||||
|
log.Error("/contact: couldn't produce table label for user %d: %s", tableID, err)
|
||||||
|
}
|
||||||
|
case "report.photo":
|
||||||
|
tableName = "photos"
|
||||||
|
|
||||||
|
// Find this photo and the user associated.
|
||||||
|
if pic, err := models.GetPhoto(uint64(tableID)); err == nil {
|
||||||
|
if user, err := models.GetUser(pic.UserID); err == nil {
|
||||||
|
tableLabel = fmt.Sprintf(`A profile photo of user account "%s"`, user.Username)
|
||||||
|
} else {
|
||||||
|
log.Error("/contact: couldn't produce table label for user %d: %s", tableID, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Error("/contact: couldn't produce table label for photo %d: %s", tableID, err)
|
||||||
|
}
|
||||||
|
case "report.message":
|
||||||
|
tableName = "messages"
|
||||||
|
tableLabel = "Direct Message conversation"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// On POST: take what we have now and email the admins.
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
// Look up the current user, in case logged in.
|
||||||
|
currentUser, err := session.CurrentUser(r)
|
||||||
|
if err == nil {
|
||||||
|
replyTo = currentUser.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store feedback in the database.
|
||||||
|
fb := &models.Feedback{
|
||||||
|
Intent: intent,
|
||||||
|
Subject: subject,
|
||||||
|
Message: message,
|
||||||
|
TableName: tableName,
|
||||||
|
TableID: uint64(tableID),
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentUser != nil && currentUser.ID > 0 {
|
||||||
|
fb.UserID = currentUser.ID
|
||||||
|
} else if replyTo != "" {
|
||||||
|
fb.ReplyTo = replyTo
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := models.CreateFeedback(fb); err != nil {
|
||||||
|
session.FlashError(w, r, "Couldn't save feedback: %s", err)
|
||||||
|
templates.Redirect(w, r.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Email the admins.
|
||||||
|
if err := mail.Send(mail.Message{
|
||||||
|
To: config.Current.AdminEmail,
|
||||||
|
Subject: "User Feedback: " + title,
|
||||||
|
Template: "email/contact_admin.html",
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"Title": title,
|
||||||
|
"Intent": intent,
|
||||||
|
"Subject": subject,
|
||||||
|
"Message": template.HTML(markdown.Render(message)),
|
||||||
|
"TableName": tableName,
|
||||||
|
"TableID": tableID,
|
||||||
|
"CurrentUser": currentUser,
|
||||||
|
"ReplyTo": replyTo,
|
||||||
|
"BaseURL": config.Current.BaseURL,
|
||||||
|
"AdminURL": config.Current.BaseURL + "/admin/feedback",
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
log.Error("/contact page: couldn't send email: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
session.Flash(w, r, success)
|
||||||
|
templates.Redirect(w, r.URL.Path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default intent = contact
|
||||||
|
if intent == "report" {
|
||||||
|
title = "Report a Problem"
|
||||||
|
} else {
|
||||||
|
intent = "contact"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the subject.
|
||||||
|
if subject != "" {
|
||||||
|
var found bool
|
||||||
|
for _, group := range config.ContactUsChoices {
|
||||||
|
for _, opt := range group.Options {
|
||||||
|
if opt.Value == subject {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
subject = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var vars = map[string]interface{}{
|
||||||
|
"Intent": intent,
|
||||||
|
"TableID": tableID,
|
||||||
|
"TableLabel": tableLabel,
|
||||||
|
"Subject": subject,
|
||||||
|
"PageTitle": title,
|
||||||
|
"Subjects": config.ContactUsChoices,
|
||||||
|
"Message": message,
|
||||||
|
"MessageRequired": messageRequired,
|
||||||
|
}
|
||||||
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -77,13 +77,14 @@ func Send(msg Message) error {
|
||||||
m.SetBody("text/plain", plaintext)
|
m.SetBody("text/plain", plaintext)
|
||||||
m.AddAlternative("text/html", html.String())
|
m.AddAlternative("text/html", html.String())
|
||||||
|
|
||||||
// Deliver.
|
// Deliver asynchronously.
|
||||||
d := gomail.NewDialer(conf.Host, conf.Port, conf.Username, conf.Password)
|
|
||||||
|
|
||||||
log.Info("mail.Send: %s (%s) to %s", msg.Subject, msg.Template, msg.To)
|
log.Info("mail.Send: %s (%s) to %s", msg.Subject, msg.Template, msg.To)
|
||||||
|
d := gomail.NewDialer(conf.Host, conf.Port, conf.Username, conf.Password)
|
||||||
|
go func() {
|
||||||
if err := d.DialAndSend(m); err != nil {
|
if err := d.DialAndSend(m); err != nil {
|
||||||
log.Error("mail.Send: %s", err.Error())
|
log.Error("mail.Send: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
89
pkg/models/feedback.go
Normal file
89
pkg/models/feedback.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/gosocial/pkg/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Feedback table for Contact Us & Reporting to admins.
|
||||||
|
type Feedback struct {
|
||||||
|
ID uint64 `gorm:"primaryKey"`
|
||||||
|
UserID uint64 `gorm:"index"` // if logged-in user posted this
|
||||||
|
Acknowledged bool `gorm:"index"` // admin dashboard "read" status
|
||||||
|
Intent string
|
||||||
|
Subject string
|
||||||
|
Message string
|
||||||
|
TableName string
|
||||||
|
TableID uint64
|
||||||
|
ReplyTo string // logged-out user may leave their email for reply
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFeedback by ID.
|
||||||
|
func GetFeedback(id uint64) (*Feedback, error) {
|
||||||
|
m := &Feedback{}
|
||||||
|
result := DB.First(&m, id)
|
||||||
|
return m, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountUnreadFeedback gets the count of unacknowledged feedback for admins.
|
||||||
|
func CountUnreadFeedback() int64 {
|
||||||
|
query := DB.Where(
|
||||||
|
"acknowledged = ?",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
result := query.Model(&Feedback{}).Count(&count)
|
||||||
|
if result.Error != nil {
|
||||||
|
log.Error("models.CountUnreadFeedback: %s", result.Error)
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// PaginateFeedback
|
||||||
|
func PaginateFeedback(acknowledged bool, intent string, pager *Pagination) ([]*Feedback, error) {
|
||||||
|
var (
|
||||||
|
fb = []*Feedback{}
|
||||||
|
wheres = []string{}
|
||||||
|
placeholders = []interface{}{}
|
||||||
|
)
|
||||||
|
|
||||||
|
wheres = append(wheres, "acknowledged = ?")
|
||||||
|
placeholders = append(placeholders, acknowledged)
|
||||||
|
|
||||||
|
if intent != "" {
|
||||||
|
wheres = append(wheres, "intent = ?")
|
||||||
|
placeholders = append(placeholders, intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
query := DB.Where(
|
||||||
|
strings.Join(wheres, " AND "),
|
||||||
|
placeholders...,
|
||||||
|
).Order(
|
||||||
|
pager.Sort,
|
||||||
|
)
|
||||||
|
|
||||||
|
query.Model(&Feedback{}).Count(&pager.Total)
|
||||||
|
|
||||||
|
result := query.Offset(
|
||||||
|
pager.GetOffset(),
|
||||||
|
).Limit(pager.PerPage).Find(&fb)
|
||||||
|
|
||||||
|
return fb, result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateFeedback saves a new Feedback row to the DB.
|
||||||
|
func CreateFeedback(fb *Feedback) error {
|
||||||
|
result := DB.Create(fb)
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save Feedback.
|
||||||
|
func (fb *Feedback) Save() error {
|
||||||
|
result := DB.Save(fb)
|
||||||
|
return result.Error
|
||||||
|
}
|
|
@ -15,4 +15,5 @@ func AutoMigrate() {
|
||||||
DB.AutoMigrate(&Message{})
|
DB.AutoMigrate(&Message{})
|
||||||
DB.AutoMigrate(&Friend{})
|
DB.AutoMigrate(&Friend{})
|
||||||
DB.AutoMigrate(&Block{})
|
DB.AutoMigrate(&Block{})
|
||||||
|
DB.AutoMigrate(&Feedback{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ func New() http.Handler {
|
||||||
mux.HandleFunc("/faq", index.FAQ())
|
mux.HandleFunc("/faq", index.FAQ())
|
||||||
mux.HandleFunc("/tos", index.TOS())
|
mux.HandleFunc("/tos", index.TOS())
|
||||||
mux.HandleFunc("/privacy", index.Privacy())
|
mux.HandleFunc("/privacy", index.Privacy())
|
||||||
|
mux.HandleFunc("/contact", index.Contact())
|
||||||
mux.HandleFunc("/login", account.Login())
|
mux.HandleFunc("/login", account.Login())
|
||||||
mux.HandleFunc("/logout", account.Logout())
|
mux.HandleFunc("/logout", account.Logout())
|
||||||
mux.HandleFunc("/signup", account.Signup())
|
mux.HandleFunc("/signup", account.Signup())
|
||||||
|
@ -57,6 +58,7 @@ func New() http.Handler {
|
||||||
// Admin endpoints.
|
// Admin endpoints.
|
||||||
mux.Handle("/admin", middleware.AdminRequired(admin.Dashboard()))
|
mux.Handle("/admin", middleware.AdminRequired(admin.Dashboard()))
|
||||||
mux.Handle("/admin/photo/certification", middleware.AdminRequired(photo.AdminCertification()))
|
mux.Handle("/admin/photo/certification", middleware.AdminRequired(photo.AdminCertification()))
|
||||||
|
mux.Handle("/admin/feedback", middleware.AdminRequired(admin.Feedback()))
|
||||||
mux.Handle("/admin/user-action", middleware.AdminRequired(admin.UserActions()))
|
mux.Handle("/admin/user-action", middleware.AdminRequired(admin.UserActions()))
|
||||||
|
|
||||||
// JSON API endpoints.
|
// JSON API endpoints.
|
||||||
|
|
|
@ -32,6 +32,7 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
|
||||||
m["NavFriendRequests"] = 0
|
m["NavFriendRequests"] = 0
|
||||||
m["NavAdminNotifications"] = 0 // total count of admin notifications for nav
|
m["NavAdminNotifications"] = 0 // total count of admin notifications for nav
|
||||||
m["NavCertificationPhotos"] = 0 // admin indicator for certification photos
|
m["NavCertificationPhotos"] = 0 // admin indicator for certification photos
|
||||||
|
m["NavAdminFeedback"] = 0 // admin indicator for unread feedback
|
||||||
m["SessionImpersonated"] = false
|
m["SessionImpersonated"] = false
|
||||||
|
|
||||||
if r == nil {
|
if r == nil {
|
||||||
|
@ -60,10 +61,16 @@ func MergeUserVars(r *http.Request, m map[string]interface{}) {
|
||||||
|
|
||||||
// Are we admin?
|
// Are we admin?
|
||||||
if user.IsAdmin {
|
if user.IsAdmin {
|
||||||
// Any pending certification photos?
|
// Any pending certification photos or feedback?
|
||||||
m["NavCertificationPhotos"] = models.CountCertificationPhotosNeedingApproval()
|
var (
|
||||||
}
|
certPhotos = models.CountCertificationPhotosNeedingApproval()
|
||||||
}
|
feedback = models.CountUnreadFeedback()
|
||||||
|
)
|
||||||
|
m["NavCertificationPhotos"] = certPhotos
|
||||||
|
m["NavAdminFeedback"] = feedback
|
||||||
|
|
||||||
m["NavAdminNotifications"] = m["NavCertificationPhotos"]
|
// Total notification count for admin actions.
|
||||||
|
m["NavAdminNotifications"] = certPhotos + feedback
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,7 +120,7 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column is-narrow has-text-centered">
|
<!-- <div class="column is-narrow has-text-centered">
|
||||||
<button type="button" class="button is-fullwidth">
|
<button type="button" class="button is-fullwidth">
|
||||||
<span class="icon-text">
|
<span class="icon-text">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
|
@ -129,17 +129,6 @@
|
||||||
<span>Like</span>
|
<span>Like</span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- <div class="column is-narrow has-text-centered">
|
|
||||||
<button type="button" class="button is-fullwidth">
|
|
||||||
<span class="icon-text">
|
|
||||||
<span class="icon">
|
|
||||||
<i class="fa fa-flag"></i>
|
|
||||||
</span>
|
|
||||||
<span>Flag</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div> -->
|
</div> -->
|
||||||
|
|
||||||
<div class="column is-narrow has-text-centered">
|
<div class="column is-narrow has-text-centered">
|
||||||
|
@ -159,6 +148,17 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="column is-narrow has-text-centered">
|
||||||
|
<a href="/contact?intent=report&subject=report.user&id={{.User.ID}}" class="button is-fullwidth">
|
||||||
|
<span class="icon-text">
|
||||||
|
<span class="icon">
|
||||||
|
<i class="fa fa-flag"></i>
|
||||||
|
</span>
|
||||||
|
<span>Report</span>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div><!-- columns -->
|
</div><!-- columns -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -29,6 +29,13 @@
|
||||||
{{if .NavCertificationPhotos}}<span class="tag is-danger ml-1">{{.NavCertificationPhotos}}</span>{{end}}
|
{{if .NavCertificationPhotos}}<span class="tag is-danger ml-1">{{.NavCertificationPhotos}}</span>{{end}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/admin/feedback">
|
||||||
|
<span class="icon"><i class="fa fa-message"></i></span>
|
||||||
|
Feedback & User Reports
|
||||||
|
{{if .NavAdminFeedback}}<span class="tag is-danger ml-1">{{.NavAdminFeedback}}</span>{{end}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
190
web/templates/admin/feedback.html
Normal file
190
web/templates/admin/feedback.html
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
{{define "title"}}Admin - Feedback & User Reports{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
{{$Root := .}}
|
||||||
|
<div class="container">
|
||||||
|
<section class="hero is-danger is-bold">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">
|
||||||
|
Feedback & User Reports
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="block p-4">
|
||||||
|
<div class="columns">
|
||||||
|
<div class="column">
|
||||||
|
There {{Pluralize64 .Pager.Total "is" "are"}} <strong>{{.Pager.Total}}</strong>
|
||||||
|
{{if .Acknowledged}}acknowledged{{else}}unread{{end}}
|
||||||
|
{{if eq .Intent "report"}}report{{else}}feedback message{{end}}{{Pluralize64 .Pager.Total}}.
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<div class="tabs is-toggle">
|
||||||
|
<ul>
|
||||||
|
<li{{if eq .Intent ""}} class="is-active"{{end}}>
|
||||||
|
<a href="{{.Request.URL.Path}}?acknowledged={{.Acknowledged}}">All</a>
|
||||||
|
</li>
|
||||||
|
<li{{if eq .Intent "contact"}} class="is-active"{{end}}>
|
||||||
|
<a href="{{.Request.URL.Path}}?acknowledged={{.Acknowledged}}&intent=contact">Contact</a>
|
||||||
|
</li>
|
||||||
|
<li{{if eq .Intent "report"}} class="is-active"{{end}}>
|
||||||
|
<a href="{{.Request.URL.Path}}?acknowledged={{.Acknowledged}}&intent=report">Reports</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<div class="tabs is-toggle">
|
||||||
|
<ul>
|
||||||
|
<li{{if not .Acknowledged}} class="is-active"{{end}}>
|
||||||
|
<a href="{{.Request.URL.Path}}?intent={{.Intent}}">Unread</a>
|
||||||
|
</li>
|
||||||
|
<li{{if .Acknowledged}} class="is-active"{{end}}>
|
||||||
|
<a href="{{.Request.URL.Path}}?acknowledged=true&intent={{.Intent}}">Acknowledged</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if .Pager}}
|
||||||
|
<nav class="pagination" role="navigation" aria-label="pagination">
|
||||||
|
<a class="pagination-previous{{if not .Pager.HasPrevious}} is-disabled{{end}}" title="Previous"
|
||||||
|
href="{{.Request.URL.Path}}?view={{.View}}&page={{.Pager.Previous}}">Previous</a>
|
||||||
|
<a class="pagination-next{{if not .Pager.HasNext}} is-disabled{{end}}" title="Next"
|
||||||
|
href="{{.Request.URL.Path}}?view={{.View}}&page={{.Pager.Next}}">Next page</a>
|
||||||
|
<ul class="pagination-list">
|
||||||
|
{{range .Pager.Iter}}
|
||||||
|
<li>
|
||||||
|
<a class="pagination-link{{if .IsCurrent}} is-current{{end}}"
|
||||||
|
aria-label="Page {{.Page}}"
|
||||||
|
href="{{$Root.Request.URL.Path}}?view={{$Root.View}}&page={{.Page}}">
|
||||||
|
{{.Page}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="columns is-multiline">
|
||||||
|
{{range .Feedback}}
|
||||||
|
<div class="column is-one-third">
|
||||||
|
{{$User := $Root.UserMap.Get .UserID}}
|
||||||
|
<form action="{{$Root.Request.URL.Path}}" method="POST">
|
||||||
|
{{InputCSRF}}
|
||||||
|
<input type="hidden" name="id" value="{{.ID}}">
|
||||||
|
|
||||||
|
<div class="card" style="max-width: 512px">
|
||||||
|
<header class="card-header {{if eq .Intent "report"}}has-background-danger{{else}}has-background-link{{end}}">
|
||||||
|
<p class="card-header-title has-text-light">
|
||||||
|
{{if eq .Intent "report"}}
|
||||||
|
<span class="icon"><i class="fa fa-flag"></i></span>
|
||||||
|
<span>Report: {{.Subject}}</span>
|
||||||
|
{{else}}
|
||||||
|
<span class="icon"><i class="fa fa-message"></i></span>
|
||||||
|
<span>Contact: {{.Subject}}</span>
|
||||||
|
{{end}}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
|
||||||
|
<table class="table is-fullwidth">
|
||||||
|
<tr>
|
||||||
|
<td class="has-text-right is-narrow">
|
||||||
|
<strong>Intent:</strong>
|
||||||
|
</td>
|
||||||
|
<td>{{.Intent}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="has-text-right">
|
||||||
|
<strong>Subject:</strong>
|
||||||
|
</td>
|
||||||
|
<td>{{.Subject}}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="has-text-right">
|
||||||
|
<strong>Table:</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{if eq .TableName ""}}
|
||||||
|
n/a
|
||||||
|
{{if ne .TableID 0}} - {{.TableID}}{{end}}
|
||||||
|
{{else if eq .TableName "users"}}
|
||||||
|
Users: {{.TableID}}
|
||||||
|
<a href="{{$Root.Request.URL.Path}}?id={{.ID}}&visit=true"
|
||||||
|
class="fa fa-external-link ml-2"
|
||||||
|
target="_blank"
|
||||||
|
title="Visit the reported user's profile"></a>
|
||||||
|
{{else if eq .TableName "photos"}}
|
||||||
|
Photos: {{.TableID}}
|
||||||
|
<a href="{{$Root.Request.URL.Path}}?id={{.ID}}&visit=true"
|
||||||
|
class="fa fa-external-link mx-2"
|
||||||
|
target="_blank"
|
||||||
|
title="Visit the reported photo"></a>
|
||||||
|
<a href="{{$Root.Request.URL.Path}}?id={{.ID}}&visit=true&profile=true"
|
||||||
|
class="fa fa-user"
|
||||||
|
target="_blank"
|
||||||
|
title="Visit the user profile who owns the reported photo"></a>
|
||||||
|
{{else if eq .TableName "messages"}}
|
||||||
|
Messages: {{.TableID}}
|
||||||
|
<a href="{{$Root.Request.URL.Path}}?id={{.ID}}&visit=true"
|
||||||
|
class="fa fa-ghost ml-2"
|
||||||
|
target="_blank"
|
||||||
|
title="Impersonate the reporter and view this message thread"></a>
|
||||||
|
{{else}}
|
||||||
|
{{.TableName}}: {{.TableID}}
|
||||||
|
<a href="{{$Root.Request.URL.Path}}?id={{.ID}}&visit=true" class="fa fa-external-link ml-2" target="_blank"></a>
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="has-text-right">
|
||||||
|
<strong>Reply To:</strong>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{if $User}}
|
||||||
|
<a href="/u/{{$User.Username}}">{{$User.Username}}</a>
|
||||||
|
{{else if ne .ReplyTo ""}}
|
||||||
|
<a href="mailto:{{.ReplyTo}}">{{.ReplyTo}}</a>
|
||||||
|
{{else}}
|
||||||
|
n/a
|
||||||
|
{{end}}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
{{if eq .Message ""}}
|
||||||
|
<p><em>No message attached.</em></p>
|
||||||
|
{{else}}
|
||||||
|
{{ToMarkdown .Message}}
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<footer class="card-footer">
|
||||||
|
{{if not .Acknowledged}}
|
||||||
|
<button type="submit" name="verdict" value="acknowledge" class="card-footer-item button is-success">
|
||||||
|
<span class="icon"><i class="fa fa-check"></i></span>
|
||||||
|
<span>Acknowledge</span>
|
||||||
|
</button>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .Acknowledged}}
|
||||||
|
<button type="submit" name="verdict" value="unacknowledge" class="card-footer-item button is-warning">
|
||||||
|
<span class="icon"><i class="fa fa-xmark"></i></span>
|
||||||
|
<span>Mark Unread</span>
|
||||||
|
</button>
|
||||||
|
{{end}}
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{{end}}
|
|
@ -92,14 +92,11 @@
|
||||||
<a class="navbar-item" href="/privacy">
|
<a class="navbar-item" href="/privacy">
|
||||||
Privacy Policy
|
Privacy Policy
|
||||||
</a>
|
</a>
|
||||||
<a class="navbar-item">
|
|
||||||
Jobs
|
|
||||||
</a>
|
|
||||||
<a class="navbar-item" href="/contact">
|
<a class="navbar-item" href="/contact">
|
||||||
Contact
|
Contact
|
||||||
</a>
|
</a>
|
||||||
<hr class="navbar-divider">
|
<hr class="navbar-divider">
|
||||||
<a class="navbar-item" href="/feedback">
|
<a class="navbar-item" href="/contact?intent=report">
|
||||||
Report an issue
|
Report an issue
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
119
web/templates/contact.html
Normal file
119
web/templates/contact.html
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
{{define "title"}}{{.PageTitle}}{{end}}
|
||||||
|
{{define "content"}}
|
||||||
|
<div class="container">
|
||||||
|
<section class="hero is-info is-bold">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">
|
||||||
|
{{.PageTitle}}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div class="block p-4">
|
||||||
|
<div class="columns is-centered">
|
||||||
|
<div class="column is-half">
|
||||||
|
|
||||||
|
<div class="card" style="width: 100%; max-width: 640px">
|
||||||
|
<header class="card-header has-background-link">
|
||||||
|
<p class="card-header-title has-text-light">
|
||||||
|
<span class="icon"><i class="fa fa-message"></i></span>
|
||||||
|
<span>{{.PageTitle}}</span>
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<div class="card-content">
|
||||||
|
|
||||||
|
<form action="/contact" method="POST">
|
||||||
|
{{InputCSRF}}
|
||||||
|
<input type="hidden" name="intent" value="{{.Intent}}">
|
||||||
|
<input type="hidden" name="id" value="{{.TableID}}">
|
||||||
|
|
||||||
|
<p class="block">
|
||||||
|
You may use this form to contact the site administrators to provide
|
||||||
|
feedback, criticism, or to report a problem you have found on the
|
||||||
|
site such as inappropriate content posted by one of our members.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="field block">
|
||||||
|
<label for="subject" class="label">Subject</label>
|
||||||
|
<div class="select is-fullwidth">
|
||||||
|
{{$Subject := .Subject}}
|
||||||
|
<select name="subject" id="subject"{{if ne $Subject ""}} disabled{{end}}>
|
||||||
|
{{range .Subjects}}
|
||||||
|
<optgroup label="{{.Header}}">
|
||||||
|
{{range .Options}}
|
||||||
|
<option value="{{.Value}}"{{if eq $Subject .Value}} selected{{end}}>{{.Label}}</option>
|
||||||
|
{{end}}
|
||||||
|
</optgroup>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- In case the selectbox is disabled -->
|
||||||
|
{{if ne .Subject ""}}<input type="hidden" name="subject" value="{{.Subject}}">{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- If they are reporting on a TableID and we have a label, show it -->
|
||||||
|
{{if ne .TableLabel ""}}
|
||||||
|
<div class="field block">
|
||||||
|
<label class="label">About</label>
|
||||||
|
<p>{{.TableLabel}}</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="field block">
|
||||||
|
<label for="message" class="label">Message{{if not .MessageRequired}} (optional){{else}} (required){{end}}</label>
|
||||||
|
<textarea class="textarea" cols="80" rows="4"
|
||||||
|
name="message"
|
||||||
|
id="message"
|
||||||
|
placeholder="Message"
|
||||||
|
{{if .MessageRequired}}required{{end}}>{{.Message}}</textarea>
|
||||||
|
{{if not .MessageRequired}}
|
||||||
|
<p class="help">
|
||||||
|
Write a description of the problem (optional).
|
||||||
|
</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if not .LoggedIn}}
|
||||||
|
<div class="field block">
|
||||||
|
<label for="email" class="label">Reply To (optional)</label>
|
||||||
|
<input type="email" class="input"
|
||||||
|
name="email" id="email"
|
||||||
|
placeholder="name@domain.com">
|
||||||
|
<p class="help">
|
||||||
|
Optional; you are not logged in to an account so you <em>MAY</em> leave us
|
||||||
|
a reply-to email address if you would like a response to your feedback.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="field has-text-centered">
|
||||||
|
<button type="submit" class="button is-success">
|
||||||
|
Send Message
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.addEventListener("DOMContentLoaded", (event) => {
|
||||||
|
let $file = document.querySelector("#file"),
|
||||||
|
$fileName = document.querySelector("#fileName");
|
||||||
|
|
||||||
|
$file.addEventListener("change", function() {
|
||||||
|
let file = this.files[0];
|
||||||
|
$fileName.innerHTML = file.name;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{{end}}
|
71
web/templates/email/contact_admin.html
Normal file
71
web/templates/email/contact_admin.html
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
{{define "content"}}
|
||||||
|
<html>
|
||||||
|
<body bakground="#ffffff" color="#000000" link="#0000FF" vlink="#990099" alink="#FF0000">
|
||||||
|
<basefont face="Arial,Helvetica,sans-serif" size="3" color="#000000"></basefont>
|
||||||
|
|
||||||
|
<h1>User Feedback: {{.Data.Title}}</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Dear website administrators,
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
A user has posted a message to you via the Contact Us form. This message
|
||||||
|
is viewable on your admin dashboard and the details are also copied below:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<strong>Intent:</strong> {{.Data.Intent}}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Subject:</strong> {{.Data.Subject}}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Table ID:</strong>
|
||||||
|
{{.Data.TableID}} ({{or .Data.TableName "n/a"}})
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Current User:</strong>
|
||||||
|
{{if .Data.CurrentUser}}
|
||||||
|
<a href="{{.Data.BaseURL}}/u/{{.Data.CurrentUser.Username}}">{{.Data.CurrentUser.Username}}</a>
|
||||||
|
(ID {{.Data.CurrentUser.ID}})
|
||||||
|
{{else}}
|
||||||
|
<em>not a logged-in user</em>
|
||||||
|
{{end}}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>Reply To:</strong>
|
||||||
|
{{if .Data.ReplyTo}}
|
||||||
|
<a href="mailto:{{.Data.ReplyTo}}">{{.Data.ReplyTo}}</a>
|
||||||
|
{{else}}
|
||||||
|
<em>User did not leave a reply-to address.</em>
|
||||||
|
{{end}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
The user's message was as follows:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
{{if ne .Data.Message ""}}
|
||||||
|
{{.Data.Message}}
|
||||||
|
{{else}}
|
||||||
|
<em>No message attached.</em>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
To view this message on the admin dashboard, please visit:
|
||||||
|
<a href="{{.Data.AdminURL}}">{{.Data.AdminURL}}</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
This is an automated e-mail; do not reply to this message.
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
{{end}}
|
|
@ -72,15 +72,19 @@
|
||||||
<div class="media block">
|
<div class="media block">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<figure class="image is-64x64">
|
<figure class="image is-64x64">
|
||||||
|
<a href="/u/{{.Username}}">
|
||||||
{{if .ProfilePhoto.ID}}
|
{{if .ProfilePhoto.ID}}
|
||||||
<img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}">
|
<img src="{{PhotoURL .ProfilePhoto.CroppedFilename}}">
|
||||||
{{else}}
|
{{else}}
|
||||||
<img src="/static/img/shy.png">
|
<img src="/static/img/shy.png">
|
||||||
{{end}}
|
{{end}}
|
||||||
|
</a>
|
||||||
</figure>
|
</figure>
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<p class="title is-4">{{.NameOrUsername}}</p>
|
<p class="title is-4">
|
||||||
|
<a href="/u/{{.Username}}" class="has-text-dark">{{.NameOrUsername}}</a>
|
||||||
|
</p>
|
||||||
<p class="subtitle is-6">
|
<p class="subtitle is-6">
|
||||||
<span class="icon"><i class="fa fa-user"></i></span>
|
<span class="icon"><i class="fa fa-user"></i></span>
|
||||||
<a href="/u/{{.Username}}">{{.Username}}</a>
|
<a href="/u/{{.Username}}">{{.Username}}</a>
|
||||||
|
|
|
@ -33,7 +33,20 @@
|
||||||
<textarea class="textarea block" cols="80" rows="4"
|
<textarea class="textarea block" cols="80" rows="4"
|
||||||
name="message"
|
name="message"
|
||||||
placeholder="Write a response"></textarea>
|
placeholder="Write a response"></textarea>
|
||||||
|
|
||||||
|
<div class="columns is-mobile">
|
||||||
|
<div class="column">
|
||||||
<button type="submit" class="button is-success">Send Reply</button>
|
<button type="submit" class="button is-success">Send Reply</button>
|
||||||
|
</div>
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<a href="/contact?intent=report&subject=report.message&id={{.MessageID}}"
|
||||||
|
class="button has-text-danger ml-4">
|
||||||
|
<span class="icon"><i class="fa fa-flag"></i></span>
|
||||||
|
<span>Report</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
|
@ -55,16 +55,14 @@
|
||||||
|
|
||||||
<!-- Reusable card footer -->
|
<!-- Reusable card footer -->
|
||||||
{{define "card-footer"}}
|
{{define "card-footer"}}
|
||||||
<footer class="card-footer">
|
|
||||||
<a class="card-footer-item" href="/photo/edit?id={{.ID}}">
|
<a class="card-footer-item" href="/photo/edit?id={{.ID}}">
|
||||||
<span class="icon"><i class="fa fa-edit"></i></span>
|
<span class="icon"><i class="fa fa-edit"></i></span>
|
||||||
Edit
|
<span>Edit</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="card-footer-item has-text-danger" href="/photo/delete?id={{.ID}}">
|
<a class="card-footer-item has-text-danger" href="/photo/delete?id={{.ID}}">
|
||||||
<span class="icon"><i class="fa fa-trash"></i></span>
|
<span class="icon"><i class="fa fa-trash"></i></span>
|
||||||
Delete
|
<span>Delete</span>
|
||||||
</a>
|
</a>
|
||||||
</footer>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<!-- Reusable pager -->
|
<!-- Reusable pager -->
|
||||||
|
@ -246,9 +244,20 @@
|
||||||
|
|
||||||
{{template "card-body" .}}
|
{{template "card-body" .}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<footer class="card-footer">
|
||||||
{{if or $Root.IsOwnPhotos $Root.CurrentUser.IsAdmin}}
|
{{if or $Root.IsOwnPhotos $Root.CurrentUser.IsAdmin}}
|
||||||
{{template "card-footer" .}}
|
{{template "card-footer" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{if not $Root.IsOwnPhotos}}
|
||||||
|
<a class="card-footer-item has-text-danger" href="/contact?intent=report&subject=report.photo&id={{.ID}}">
|
||||||
|
<span class="icon"><i class="fa fa-flag"></i></span>
|
||||||
|
<span>Report</span>
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
</footer>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
@ -305,9 +314,19 @@
|
||||||
|
|
||||||
{{template "card-body" .}}
|
{{template "card-body" .}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<footer class="card-footer">
|
||||||
{{if or $Root.IsOwnPhotos $Root.CurrentUser.IsAdmin}}
|
{{if or $Root.IsOwnPhotos $Root.CurrentUser.IsAdmin}}
|
||||||
{{template "card-footer" .}}
|
{{template "card-footer" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
{{if not $Root.IsOwnPhotos}}
|
||||||
|
<a class="card-footer-item has-text-danger" href="/contact?intent=report&subject=report.photo&id={{.ID}}">
|
||||||
|
<span class="icon"><i class="fa fa-flag"></i></span>
|
||||||
|
<span>Report</span>
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user