Contact creation and invite UI
This commit is contained in:
parent
765e80b64d
commit
345878fabe
2
blog.go
2
blog.go
|
@ -26,6 +26,7 @@ import (
|
||||||
"github.com/kirsle/blog/jsondb/caches/null"
|
"github.com/kirsle/blog/jsondb/caches/null"
|
||||||
"github.com/kirsle/blog/jsondb/caches/redis"
|
"github.com/kirsle/blog/jsondb/caches/redis"
|
||||||
"github.com/kirsle/blog/models/comments"
|
"github.com/kirsle/blog/models/comments"
|
||||||
|
"github.com/kirsle/blog/models/contacts"
|
||||||
"github.com/kirsle/blog/models/events"
|
"github.com/kirsle/blog/models/events"
|
||||||
"github.com/kirsle/blog/models/posts"
|
"github.com/kirsle/blog/models/posts"
|
||||||
"github.com/kirsle/blog/models/settings"
|
"github.com/kirsle/blog/models/settings"
|
||||||
|
@ -90,6 +91,7 @@ func (b *Blog) Configure() {
|
||||||
posts.DB = b.DB
|
posts.DB = b.DB
|
||||||
users.DB = b.DB
|
users.DB = b.DB
|
||||||
comments.DB = b.DB
|
comments.DB = b.DB
|
||||||
|
contacts.DB = b.DB
|
||||||
events.DB = b.DB
|
events.DB = b.DB
|
||||||
|
|
||||||
// Redis cache?
|
// Redis cache?
|
||||||
|
|
|
@ -18,7 +18,7 @@ func Register(r *mux.Router, loginError http.HandlerFunc) {
|
||||||
// Login-required routers.
|
// Login-required routers.
|
||||||
loginRouter := mux.NewRouter()
|
loginRouter := mux.NewRouter()
|
||||||
loginRouter.HandleFunc("/e/admin/edit", editHandler)
|
loginRouter.HandleFunc("/e/admin/edit", editHandler)
|
||||||
loginRouter.HandleFunc("/e/admin/invite", inviteHandler)
|
loginRouter.HandleFunc("/e/admin/invite/{id}", inviteHandler)
|
||||||
loginRouter.HandleFunc("/e/admin/", indexHandler)
|
loginRouter.HandleFunc("/e/admin/", indexHandler)
|
||||||
r.PathPrefix("/e/admin").Handler(
|
r.PathPrefix("/e/admin").Handler(
|
||||||
negroni.New(
|
negroni.New(
|
||||||
|
|
|
@ -3,28 +3,101 @@ package events
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/kirsle/blog/internal/log"
|
||||||
"github.com/kirsle/blog/internal/render"
|
"github.com/kirsle/blog/internal/render"
|
||||||
"github.com/kirsle/blog/internal/responses"
|
"github.com/kirsle/blog/internal/responses"
|
||||||
|
"github.com/kirsle/blog/models/contacts"
|
||||||
"github.com/kirsle/blog/models/events"
|
"github.com/kirsle/blog/models/events"
|
||||||
)
|
)
|
||||||
|
|
||||||
func inviteHandler(w http.ResponseWriter, r *http.Request) {
|
func inviteHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
v := map[string]interface{}{
|
params := mux.Vars(r)
|
||||||
"preview": "",
|
id, err := strconv.Atoi(params["id"])
|
||||||
}
|
|
||||||
|
|
||||||
id, err := strconv.Atoi(r.FormValue("id"))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
responses.FlashAndRedirect(w, r, "/e/admin/", "Invalid ID")
|
responses.FlashAndRedirect(w, r, "/e/admin/", "Invalid ID")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load the event from its ID.
|
||||||
event, err := events.Load(id)
|
event, err := events.Load(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
responses.FlashAndRedirect(w, r, "/e/admin/", "Can't load event: %s", err)
|
responses.FlashAndRedirect(w, r, "/e/admin/", "Can't load event: %s", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
v["event"] = event
|
// Get the address book.
|
||||||
|
addr, _ := contacts.Load()
|
||||||
|
|
||||||
|
// Handle POST requests.
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
action := r.FormValue("action")
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "new-contact":
|
||||||
|
c := contacts.NewContact()
|
||||||
|
c.ParseForm(r)
|
||||||
|
err = c.Validate()
|
||||||
|
if err != nil {
|
||||||
|
responses.FlashAndReload(w, r, "Validation error: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addr.Add(c)
|
||||||
|
err = addr.Save()
|
||||||
|
if err != nil {
|
||||||
|
responses.FlashAndReload(w, r, "Error when saving address book: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
responses.FlashAndReload(w, r, "Added %s to the address book!", c.Name())
|
||||||
|
return
|
||||||
|
case "send-invite":
|
||||||
|
log.Error("Send Invite!")
|
||||||
|
r.ParseForm()
|
||||||
|
contactIDs, ok := r.Form["invite"]
|
||||||
|
if !ok {
|
||||||
|
responses.Error(w, r, "Missing: invite (list of IDs)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invite all the users.
|
||||||
|
var warnings []string
|
||||||
|
for _, strID := range contactIDs {
|
||||||
|
id, _ := strconv.Atoi(strID)
|
||||||
|
err := event.InviteContactID(id)
|
||||||
|
if err != nil {
|
||||||
|
warnings = append(warnings, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(warnings) > 0 {
|
||||||
|
responses.Flash(w, r, "Warnings: %s", strings.Join(warnings, "; "))
|
||||||
|
}
|
||||||
|
responses.FlashAndReload(w, r, "Invites sent!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invited, err := event.Invited()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("error getting event.Invited: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map the invited user IDs.
|
||||||
|
invitedMap := map[int]bool{}
|
||||||
|
for _, rsvp := range invited {
|
||||||
|
if rsvp.ContactID != 0 {
|
||||||
|
invitedMap[rsvp.ContactID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v := map[string]interface{}{
|
||||||
|
"event": event,
|
||||||
|
"invited": invited,
|
||||||
|
"invitedMap": invitedMap,
|
||||||
|
"contacts": addr,
|
||||||
|
}
|
||||||
render.Template(w, r, "events/invite", v)
|
render.Template(w, r, "events/invite", v)
|
||||||
}
|
}
|
||||||
|
|
164
models/contacts/contacts.go
Normal file
164
models/contacts/contacts.go
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
package contacts
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kirsle/blog/jsondb"
|
||||||
|
"github.com/kirsle/golog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DB is a reference to the parent app's JsonDB object.
|
||||||
|
var DB *jsondb.DB
|
||||||
|
|
||||||
|
var log *golog.Logger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = golog.GetLogger("blog")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contacts is an address book of users who have been invited to events.
|
||||||
|
type Contacts struct {
|
||||||
|
Serial int `json:"serial"`
|
||||||
|
Contacts []*Contact `json:"contacts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contact is an individual contact in the address book.
|
||||||
|
type Contact struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
FirstName string `json:"firstName"`
|
||||||
|
LastName string `json:"lastName"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
SMS string `json:"sms"`
|
||||||
|
LastSeen time.Time `json:"lastSeen"`
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContact initializes a new contact entry.
|
||||||
|
func NewContact() *Contact {
|
||||||
|
return &Contact{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the singleton contact list.
|
||||||
|
func Load() (*Contacts, error) {
|
||||||
|
c := &Contacts{
|
||||||
|
Serial: 1,
|
||||||
|
Contacts: []*Contact{},
|
||||||
|
}
|
||||||
|
if DB.Exists("contacts/address-book") {
|
||||||
|
err := DB.Get("contacts/address-book", &c)
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new contact.
|
||||||
|
func (cl *Contacts) Add(c *Contact) {
|
||||||
|
if c.ID == 0 {
|
||||||
|
c.ID = cl.Serial
|
||||||
|
cl.Serial++
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Created.IsZero() {
|
||||||
|
c.Created = time.Now().UTC()
|
||||||
|
}
|
||||||
|
if c.Updated.IsZero() {
|
||||||
|
c.Updated = time.Now().UTC()
|
||||||
|
}
|
||||||
|
cl.Contacts = append(cl.Contacts, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the contact list.
|
||||||
|
func (cl *Contacts) Save() error {
|
||||||
|
sort.Sort(ByName(cl.Contacts))
|
||||||
|
return DB.Commit("contacts/address-book", cl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID queries a contact by its ID number.
|
||||||
|
func (cl *Contacts) GetID(id int) (*Contact, error) {
|
||||||
|
for _, c := range cl.Contacts {
|
||||||
|
if c.ID == id {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEmail queries a contact by email address.
|
||||||
|
func (cl *Contacts) GetEmail(email string) (*Contact, error) {
|
||||||
|
email = strings.ToLower(email)
|
||||||
|
for _, c := range cl.Contacts {
|
||||||
|
if c.Email == email {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSMS queries a contact by SMS number.
|
||||||
|
func (cl *Contacts) GetSMS(number string) (*Contact, error) {
|
||||||
|
for _, c := range cl.Contacts {
|
||||||
|
if c.SMS == number {
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, errors.New("not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name returns a friendly name for the contact.
|
||||||
|
func (c *Contact) Name() string {
|
||||||
|
var parts []string
|
||||||
|
if c.FirstName != "" {
|
||||||
|
parts = append(parts, c.FirstName)
|
||||||
|
}
|
||||||
|
if c.LastName != "" {
|
||||||
|
parts = append(parts, c.LastName)
|
||||||
|
}
|
||||||
|
if len(parts) == 0 {
|
||||||
|
if c.Email != "" {
|
||||||
|
parts = append(parts, c.Email)
|
||||||
|
} else if c.SMS != "" {
|
||||||
|
parts = append(parts, c.SMS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseForm accepts form data for a contact.
|
||||||
|
func (c *Contact) ParseForm(r *http.Request) {
|
||||||
|
c.FirstName = r.FormValue("first_name")
|
||||||
|
c.LastName = r.FormValue("last_name")
|
||||||
|
c.Email = strings.ToLower(r.FormValue("email"))
|
||||||
|
c.SMS = r.FormValue("sms")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the contact form.
|
||||||
|
func (c *Contact) Validate() error {
|
||||||
|
if c.Email == "" && c.SMS == "" {
|
||||||
|
return errors.New("email or sms number required")
|
||||||
|
}
|
||||||
|
if c.FirstName == "" && c.LastName == "" {
|
||||||
|
return errors.New("first or last name required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the address book out.
|
||||||
|
addr, _ := Load()
|
||||||
|
|
||||||
|
// Check for uniqueness of email and SMS.
|
||||||
|
if c.Email != "" {
|
||||||
|
if _, err := addr.GetEmail(c.Email); err == nil {
|
||||||
|
return errors.New("email address already exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.SMS != "" {
|
||||||
|
if _, err := addr.GetSMS(c.SMS); err == nil {
|
||||||
|
return errors.New("sms number already exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
10
models/contacts/sorting.go
Normal file
10
models/contacts/sorting.go
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
package contacts
|
||||||
|
|
||||||
|
// ByName sorts contacts by name.
|
||||||
|
type ByName []*Contact
|
||||||
|
|
||||||
|
func (a ByName) Len() int { return len(a) }
|
||||||
|
func (a ByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
|
func (a ByName) Less(i, j int) bool {
|
||||||
|
return a[i].Name() < a[j].Name()
|
||||||
|
}
|
|
@ -39,21 +39,6 @@ type Event struct {
|
||||||
Updated time.Time `json:"updated"`
|
Updated time.Time `json:"updated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// RSVP tracks invitations and confirmations to events.
|
|
||||||
type RSVP struct {
|
|
||||||
// If the user was invited by an admin, they will have a ContactID and
|
|
||||||
// not much else. Users who signed up themselves from an OpenSignup event
|
|
||||||
// will have the metadata filled in instead.
|
|
||||||
ContactID int `json:"contactId"`
|
|
||||||
Notified bool `json:"notified"`
|
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
Status string `json:"status,omitempty"` // invited, going, maybe, not going
|
|
||||||
Email string `json:"email,omitempty"`
|
|
||||||
SMS string `json:"sms,omitempty"`
|
|
||||||
Created time.Time `json:"created"`
|
|
||||||
Updated time.Time `json:"updated"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a blank event with sensible defaults.
|
// New creates a blank event with sensible defaults.
|
||||||
func New() *Event {
|
func New() *Event {
|
||||||
return &Event{
|
return &Event{
|
||||||
|
|
70
models/events/invites.go
Normal file
70
models/events/invites.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package events
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kirsle/blog/models/contacts"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RSVP status constants.
|
||||||
|
const (
|
||||||
|
StatusInvited = "invited"
|
||||||
|
StatusGoing = "going"
|
||||||
|
StatusMaybe = "maybe"
|
||||||
|
StatusNotGoing = "not going"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RSVP tracks invitations and confirmations to events.
|
||||||
|
type RSVP struct {
|
||||||
|
// If the user was invited by an admin, they will have a ContactID and
|
||||||
|
// not much else. Users who signed up themselves from an OpenSignup event
|
||||||
|
// will have the metadata filled in instead.
|
||||||
|
ContactID int `json:"contactId"`
|
||||||
|
Contact *contacts.Contact `json:"-"` // rel table not serialized to JSON
|
||||||
|
Status string `json:"status"` // invited, going, maybe, not going
|
||||||
|
Notified bool `json:"notified"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Email string `json:"email,omitempty"`
|
||||||
|
SMS string `json:"sms,omitempty"`
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// InviteContactID enters an invitation for a contact ID.
|
||||||
|
func (ev *Event) InviteContactID(id int) error {
|
||||||
|
// Make sure the ID isn't already in the list.
|
||||||
|
for _, rsvp := range ev.RSVP {
|
||||||
|
if rsvp.ContactID != 0 && rsvp.ContactID == id {
|
||||||
|
return errors.New("already invited")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ev.RSVP = append(ev.RSVP, RSVP{
|
||||||
|
ContactID: id,
|
||||||
|
Status: StatusInvited,
|
||||||
|
Created: time.Now().UTC(),
|
||||||
|
Updated: time.Now().UTC(),
|
||||||
|
})
|
||||||
|
return ev.Save()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invited returns the RSVPs with Contact objects injected for contacts.
|
||||||
|
func (ev *Event) Invited() ([]RSVP, error) {
|
||||||
|
cl, _ := contacts.Load()
|
||||||
|
result := []RSVP{}
|
||||||
|
for _, rsvp := range ev.RSVP {
|
||||||
|
if rsvp.ContactID != 0 {
|
||||||
|
fmt.Printf("cid: %d\n", rsvp.ContactID)
|
||||||
|
c, err := cl.GetID(rsvp.ContactID)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("event.Invited error: %s", err)
|
||||||
|
}
|
||||||
|
rsvp.Contact = c
|
||||||
|
}
|
||||||
|
result = append(result, rsvp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
|
@ -6,5 +6,5 @@ type ByDate []*Event
|
||||||
func (a ByDate) Len() int { return len(a) }
|
func (a ByDate) Len() int { return len(a) }
|
||||||
func (a ByDate) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
func (a ByDate) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||||
func (a ByDate) Less(i, j int) bool {
|
func (a ByDate) Less(i, j int) bool {
|
||||||
return a[i].StartTime.Before(a[i].StartTime) || a[i].ID < a[j].ID
|
return a[i].StartTime.Before(a[j].StartTime) || a[i].ID < a[j].ID
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
{{ define "title" }}Invite People to {{ .Data.event.Title }}{{ end }}
|
{{ define "title" }}Invite People to {{ .Data.event.Title }}{{ end }}
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
<form action="/e/admin/edit" method="POST">
|
|
||||||
<input type="hidden" name="_csrf" value="{{ .CSRF }}">
|
|
||||||
|
|
||||||
{{ $e := .Data.event }}
|
{{ $e := .Data.event }}
|
||||||
<h1>Invite <em>{{ $e.Title }}</em></h1>
|
{{ $cl := .Data.contacts }}
|
||||||
|
|
||||||
<div class="card mb-4">
|
<h1>Invite <em>{{ $e.Title }}</em></h1>
|
||||||
|
|
||||||
|
<div class="card mb-4">
|
||||||
<div class="card-header">Contact List</div>
|
<div class="card-header">Contact List</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row" style="max-height: 500px; overflow: auto">
|
<div class="row" style="max-height: 500px; overflow: auto">
|
||||||
|
@ -14,6 +14,42 @@
|
||||||
<h4>Invited</h4>
|
<h4>Invited</h4>
|
||||||
|
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
|
{{ range .Data.invited }}
|
||||||
|
<li>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{{ if .Contact }}
|
||||||
|
<strong>{{ .Contact.Name }}</strong>
|
||||||
|
{{ else }}
|
||||||
|
<strong>{{ .Name }}</strong>
|
||||||
|
{{ end }}
|
||||||
|
<ul class="list-inline">
|
||||||
|
{{ if .Contact }}
|
||||||
|
{{ if .Contact.Email }}
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<a href="mailto:{{ .Contact.Email }}">{{ .Contact.Email }}</a>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
{{ if .Contact.SMS }}
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<a href="tel:{{ .Contact.SMS }}">{{ .Contact.SMS }}</a>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
{{ else }}
|
||||||
|
{{ if .Email }}
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<a href="mailto:{{ .Email }}">{{ .Email }}</a>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
{{ if .SMS }}
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<a href="tel:{{ .SMS }}">{{ .SMS }}</a>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
<li>
|
<li>
|
||||||
<strong>John Doe</strong><br>
|
<strong>John Doe</strong><br>
|
||||||
<span class="text-muted">name@example.com</span>
|
<span class="text-muted">name@example.com</span>
|
||||||
|
@ -27,7 +63,32 @@
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<h4>Available</h4>
|
<h4>Available</h4>
|
||||||
|
|
||||||
|
<form action="/e/admin/invite/{{ $e.ID }}" method="POST">
|
||||||
|
<input type="hidden" name="_csrf" value="{{ .CSRF }}">
|
||||||
|
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
|
{{ range $cl.Contacts }}
|
||||||
|
{{ if not (index $.Data.invitedMap .ID) }}
|
||||||
|
<li>
|
||||||
|
<label class="d-block alert alert-info">
|
||||||
|
<input type="checkbox" name="invite" value="{{ .ID }}">
|
||||||
|
<strong>{{ .Name }}</strong>
|
||||||
|
<ul class="list-inline">
|
||||||
|
{{ if .Email }}
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<a href="mailto:{{ .Email }}">{{ .Email }}</a>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
{{ if .SMS }}
|
||||||
|
<li class="list-inline-item">
|
||||||
|
<a href="tel:{{ .SMS }}">{{ .SMS }}</a>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
</ul>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
<li>
|
<li>
|
||||||
<label class="d-block alert alert-info">
|
<label class="d-block alert alert-info">
|
||||||
<input type="checkbox" name="invite" value="1">
|
<input type="checkbox" name="invite" value="1">
|
||||||
|
@ -47,14 +108,21 @@
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
name="action" value="send-invite"
|
name="action" value="send-invite"
|
||||||
class="btn btn-primary">Send Invites</button>
|
class="btn btn-primary">Send Invites</button>
|
||||||
</div>
|
<a href="/admin/contacts"
|
||||||
</div>
|
class="btn btn-secondary">Manage Contacts</a>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card">
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
<div class="card-header">Invite New People</div>
|
<div class="card-header">Invite New People</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
|
<form action="/e/admin/invite/{{ $e.ID }}" method="POST">
|
||||||
|
<input type="hidden" name="_csrf" value="{{ .CSRF }}">
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group col-md-6">
|
<div class="form-group col-md-6">
|
||||||
<label for="first_name">First name:</label>
|
<label for="first_name">First name:</label>
|
||||||
|
@ -94,15 +162,18 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<button type="button"
|
<button type="submit"
|
||||||
|
name="action"
|
||||||
|
value="new-contact"
|
||||||
class="btn btn-primary">Send Invite</button>
|
class="btn btn-primary">Send Invite</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>Invited</h2>
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
To Do
|
<h2>Invited</h2>
|
||||||
</form>
|
|
||||||
|
To Do
|
||||||
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
<a href="/e/admin/edit?id={{ .Data.event.ID }}"
|
<a href="/e/admin/edit?id={{ .Data.event.ID }}"
|
||||||
class="btn btn-primary">edit event</a>
|
class="btn btn-primary">edit event</a>
|
||||||
<a href="/e/admin/invite?id={{ .Data.event.ID }}"
|
<a href="/e/admin/invite/{{ .Data.event.ID }}"
|
||||||
class="btn btn-success">invite people</a>
|
class="btn btn-success">invite people</a>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user