Compare commits
3 Commits
2fd053777d
...
db5352bc9c
| Author | SHA1 | Date | |
|---|---|---|---|
| db5352bc9c | |||
| dd83081271 | |||
| e3428911b9 |
@@ -11,13 +11,37 @@ import (
|
||||
|
||||
func MessagesInboxHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := helpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
helpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
page := helpers.Atoi(r.URL.Query().Get("page"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
perPage := 10
|
||||
|
||||
totalCount := storage.GetInboxMessageCount(db, userID)
|
||||
totalPages := (totalCount + perPage - 1) / perPage
|
||||
if totalPages == 0 {
|
||||
totalPages = 1
|
||||
}
|
||||
|
||||
messages := storage.GetInboxMessages(db, userID, page, perPage)
|
||||
|
||||
data := BuildTemplateData(db, w, r)
|
||||
context := helpers.TemplateContext(w, r, data)
|
||||
|
||||
context["Messages"] = messages
|
||||
context["CurrentPage"] = page
|
||||
context["TotalPages"] = totalPages
|
||||
context["PageRange"] = helpers.PageRange(page, totalPages)
|
||||
|
||||
tmpl := helpers.LoadTemplateFiles("messages.html", "templates/account/messages/index.html")
|
||||
|
||||
err := tmpl.ExecuteTemplate(w, "layout", context)
|
||||
if err != nil {
|
||||
if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil {
|
||||
helpers.RenderError(w, r, 500)
|
||||
}
|
||||
}
|
||||
@@ -52,3 +76,107 @@ func ReadMessageHandler(db *sql.DB) http.HandlerFunc {
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
}
|
||||
}
|
||||
|
||||
func ArchiveMessageHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := helpers.Atoi(r.URL.Query().Get("id"))
|
||||
userID, ok := helpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
helpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
err := storage.ArchiveMessage(db, userID, id)
|
||||
if err != nil {
|
||||
helpers.SetFlash(w, r, "Failed to archive message.")
|
||||
} else {
|
||||
helpers.SetFlash(w, r, "Message archived.")
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/account/messages", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
func ArchivedMessagesHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, ok := helpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
helpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
page := helpers.Atoi(r.URL.Query().Get("page"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
perPage := 10
|
||||
|
||||
messages := storage.GetArchivedMessages(db, userID, page, perPage)
|
||||
hasMore := len(messages) == perPage
|
||||
|
||||
data := BuildTemplateData(db, w, r)
|
||||
context := helpers.TemplateContext(w, r, data)
|
||||
context["Messages"] = messages
|
||||
context["Page"] = page
|
||||
context["HasMore"] = hasMore
|
||||
|
||||
tmpl := helpers.LoadTemplateFiles("archived.html", "templates/account/messages/archived.html")
|
||||
tmpl.ExecuteTemplate(w, "layout", context)
|
||||
}
|
||||
}
|
||||
|
||||
func SendMessageHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
// Display the form
|
||||
data := BuildTemplateData(db, w, r)
|
||||
context := helpers.TemplateContext(w, r, data)
|
||||
tmpl := helpers.LoadTemplateFiles("send-message.html", "templates/account/messages/send.html")
|
||||
|
||||
if err := tmpl.ExecuteTemplate(w, "layout", context); err != nil {
|
||||
helpers.RenderError(w, r, 500)
|
||||
}
|
||||
case http.MethodPost:
|
||||
// Handle form submission
|
||||
senderID, ok := helpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
helpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
recipientID := helpers.Atoi(r.FormValue("recipient_id"))
|
||||
subject := r.FormValue("subject")
|
||||
body := r.FormValue("message")
|
||||
|
||||
if err := storage.SendMessage(db, senderID, recipientID, subject, body); err != nil {
|
||||
helpers.SetFlash(w, r, "Failed to send message.")
|
||||
} else {
|
||||
helpers.SetFlash(w, r, "Message sent.")
|
||||
}
|
||||
http.Redirect(w, r, "/account/messages", http.StatusSeeOther)
|
||||
default:
|
||||
helpers.RenderError(w, r, 405)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func RestoreMessageHandler(db *sql.DB) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
id := helpers.Atoi(r.URL.Query().Get("id"))
|
||||
userID, ok := helpers.GetCurrentUserID(r)
|
||||
if !ok {
|
||||
helpers.RenderError(w, r, 403)
|
||||
return
|
||||
}
|
||||
|
||||
err := storage.RestoreMessage(db, userID, id)
|
||||
if err != nil {
|
||||
helpers.SetFlash(w, r, "Failed to restore message.")
|
||||
} else {
|
||||
helpers.SetFlash(w, r, "Message restored.")
|
||||
}
|
||||
|
||||
http.Redirect(w, r, "/account/messages/archived", http.StatusSeeOther)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,3 +16,11 @@ func GetTotalPages(db *sql.DB, tableName, whereClause string, args []interface{}
|
||||
}
|
||||
return totalPages, totalCount
|
||||
}
|
||||
|
||||
func MakePageRange(current, total int) []int {
|
||||
var pages []int
|
||||
for i := 1; i <= total; i++ {
|
||||
pages = append(pages, i)
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ func TemplateFuncs() template.FuncMap {
|
||||
},
|
||||
"mul": func(a, b int) int { return a * b },
|
||||
"add": func(a, b int) int { return a + b },
|
||||
"sub": func(a, b int) int { return a - b },
|
||||
"min": func(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
@@ -66,6 +67,7 @@ func TemplateFuncs() template.FuncMap {
|
||||
}
|
||||
return s[:max] + "..."
|
||||
},
|
||||
"PageRange": PageRange,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,3 +117,11 @@ func rangeClass(n int) string {
|
||||
return "50-plus"
|
||||
}
|
||||
}
|
||||
|
||||
func PageRange(current, total int) []int {
|
||||
var pages []int
|
||||
for i := 1; i <= total; i++ {
|
||||
pages = append(pages, i)
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
4
main.go
4
main.go
@@ -70,6 +70,10 @@ func setupAccountRoutes(mux *http.ServeMux, db *sql.DB) {
|
||||
mux.HandleFunc("/account/tickets/my_tickets", handlers.GetMyTickets(db))
|
||||
mux.HandleFunc("/account/messages", middleware.Auth(true)(handlers.MessagesInboxHandler(db)))
|
||||
mux.HandleFunc("/account/messages/read", middleware.Auth(true)(handlers.ReadMessageHandler(db)))
|
||||
mux.HandleFunc("/account/messages/archive", middleware.Auth(true)(handlers.ArchiveMessageHandler(db)))
|
||||
mux.HandleFunc("/account/messages/archived", middleware.Auth(true)(handlers.ArchivedMessagesHandler(db)))
|
||||
mux.HandleFunc("/account/messages/restore", middleware.Auth(true)(handlers.RestoreMessageHandler(db)))
|
||||
mux.HandleFunc("/account/messages/send", middleware.Auth(true)(handlers.SendMessageHandler(db)))
|
||||
mux.HandleFunc("/account/notifications", middleware.Auth(true)(handlers.NotificationsHandler(db)))
|
||||
mux.HandleFunc("/account/notifications/read", middleware.Auth(true)(handlers.MarkNotificationReadHandler(db)))
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ type Message struct {
|
||||
Message string
|
||||
IsRead bool
|
||||
CreatedAt time.Time
|
||||
ArchivedAt *time.Time
|
||||
}
|
||||
|
||||
var db *sql.DB
|
||||
|
||||
@@ -10,7 +10,7 @@ func GetMessageCount(db *sql.DB, userID int) (int, error) {
|
||||
var count int
|
||||
err := db.QueryRow(`
|
||||
SELECT COUNT(*) FROM users_messages
|
||||
WHERE recipientId = ? AND is_read = FALSE
|
||||
WHERE recipientId = ? AND is_read = FALSE AND is_archived = FALSE
|
||||
`, userID).Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
@@ -19,7 +19,7 @@ func GetRecentMessages(db *sql.DB, userID int, limit int) []models.Message {
|
||||
rows, err := db.Query(`
|
||||
SELECT id, senderId, recipientId, subject, message, is_read, created_at
|
||||
FROM users_messages
|
||||
WHERE recipientId = ?
|
||||
WHERE recipientId = ? AND is_archived = FALSE
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
`, userID, limit)
|
||||
@@ -81,3 +81,98 @@ func MarkMessageAsRead(db *sql.DB, messageID, userID int) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ArchiveMessage(db *sql.DB, userID, messageID int) error {
|
||||
_, err := db.Exec(`
|
||||
UPDATE users_messages
|
||||
SET is_archived = TRUE, archived_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND recipientId = ?
|
||||
`, messageID, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
func SendMessage(db *sql.DB, senderID, recipientID int, subject, message string) error {
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO users_messages (senderId, recipientId, subject, message)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`, senderID, recipientID, subject, message)
|
||||
return err
|
||||
}
|
||||
|
||||
func GetArchivedMessages(db *sql.DB, userID int, page, perPage int) []models.Message {
|
||||
offset := (page - 1) * perPage
|
||||
rows, err := db.Query(`
|
||||
SELECT id, senderId, recipientId, subject, message, is_read, created_at, archived_at
|
||||
FROM users_messages
|
||||
WHERE recipientId = ? AND is_archived = TRUE
|
||||
ORDER BY archived_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`, userID, perPage, offset)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var messages []models.Message
|
||||
for rows.Next() {
|
||||
var m models.Message
|
||||
err := rows.Scan(
|
||||
&m.ID, &m.SenderId, &m.RecipientId,
|
||||
&m.Subject, &m.Message, &m.IsRead,
|
||||
&m.CreatedAt, &m.ArchivedAt,
|
||||
)
|
||||
if err == nil {
|
||||
messages = append(messages, m)
|
||||
}
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
func GetInboxMessages(db *sql.DB, userID int, page, perPage int) []models.Message {
|
||||
offset := (page - 1) * perPage
|
||||
rows, err := db.Query(`
|
||||
SELECT id, senderId, recipientId, subject, message, is_read, created_at
|
||||
FROM users_messages
|
||||
WHERE recipientId = ? AND is_archived = FALSE
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`, userID, perPage, offset)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var messages []models.Message
|
||||
for rows.Next() {
|
||||
var m models.Message
|
||||
err := rows.Scan(
|
||||
&m.ID, &m.SenderId, &m.RecipientId,
|
||||
&m.Subject, &m.Message, &m.IsRead, &m.CreatedAt,
|
||||
)
|
||||
if err == nil {
|
||||
messages = append(messages, m)
|
||||
}
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
func GetInboxMessageCount(db *sql.DB, userID int) int {
|
||||
var count int
|
||||
err := db.QueryRow(`
|
||||
SELECT COUNT(*) FROM users_messages
|
||||
WHERE recipientId = ? AND is_archived = FALSE
|
||||
`, userID).Scan(&count)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func RestoreMessage(db *sql.DB, userID, messageID int) error {
|
||||
_, err := db.Exec(`
|
||||
UPDATE users_messages
|
||||
SET is_archived = FALSE, archived_at = NULL
|
||||
WHERE id = ? AND recipientId = ?
|
||||
`, messageID, userID)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -115,11 +115,13 @@ const SchemaUsersMessages = `
|
||||
CREATE TABLE IF NOT EXISTS users_messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
senderId INTEGER NOT NULL REFERENCES users(id),
|
||||
recipientId int,
|
||||
recipientId INTEGER NOT NULL REFERENCES users(id),
|
||||
subject TEXT NOT NULL,
|
||||
message TEXT,
|
||||
is_read BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
is_archived BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
archived_at TIMESTAMP
|
||||
);`
|
||||
|
||||
const SchemaUsersNotifications = `
|
||||
|
||||
44
templates/account/messages/archived.html
Normal file
44
templates/account/messages/archived.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-4">
|
||||
<h2>Archived Messages</h2>
|
||||
|
||||
{{ if .Messages }}
|
||||
{{ range .Messages }}
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">{{ .Subject }}</h5>
|
||||
<p class="card-text">{{ .Message }}</p>
|
||||
<p class="card-text">
|
||||
<small class="text-muted">Archived: {{ .ArchivedAt.Format "02 Jan 2006 15:04" }}</small>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<form method="POST" action="/account/messages/restore" class="m-0">
|
||||
{{ $.CSRFField }}
|
||||
<input type="hidden" name="id" value="{{ .ID }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-success">Restore</button>
|
||||
</form>
|
||||
{{ end }}
|
||||
|
||||
<!-- Pagination Controls -->
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
{{ if gt .Page 1 }}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ minus1 .Page }}">Previous</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
{{ if .HasMore }}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ plus1 .Page }}">Next</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</nav>
|
||||
{{ else }}
|
||||
<div class="alert alert-info text-center">No archived messages.</div>
|
||||
{{ end }}
|
||||
|
||||
<a href="/account/messages" class="btn btn-secondary mt-3">Back to Inbox</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -1,22 +1,54 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-4">
|
||||
<h2>Your Messages</h2>
|
||||
<ul class="list-group mt-3">
|
||||
<div class="container py-5">
|
||||
<h2>Your Inbox</h2>
|
||||
{{ if .Messages }}
|
||||
<ul class="list-group mb-4">
|
||||
{{ range .Messages }}
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<a href="/account/messages/read?id={{ .ID }}" class="{{ if not .IsRead }}fw-bold{{ end }}">
|
||||
{{ .Subject }}
|
||||
</a>
|
||||
<div class="small text-muted">{{ .CreatedAt.Format "02 Jan 2006 15:04" }}</div>
|
||||
</div>
|
||||
{{ if not .IsRead }}
|
||||
<span class="badge bg-primary">Unread</span>
|
||||
{{ end }}
|
||||
</li>
|
||||
{{ else }}
|
||||
<li class="list-group-item text-muted text-center">No messages</li>
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<a href="/account/messages/read?id={{ .ID }}" class="fw-bold text-dark">{{ .Subject }}</a>
|
||||
<br>
|
||||
<small class="text-muted">{{ .CreatedAt.Format "02 Jan 2006 15:04" }}</small>
|
||||
</div>
|
||||
<form method="POST" action="/account/messages/archive?id={{ .ID }}" class="m-0">
|
||||
{{ $.CSRFField }}
|
||||
<input type="hidden" name="id" value="{{ .ID }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-secondary">Archive</button>
|
||||
</form>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<!-- Pagination -->
|
||||
<nav>
|
||||
<ul class="pagination">
|
||||
{{ if gt .CurrentPage 1 }}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ sub .CurrentPage 1 }}">Previous</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
|
||||
{{ range $i := .PageRange }}
|
||||
<li class="page-item {{ if eq $i $.CurrentPage }}active{{ end }}">
|
||||
<a class="page-link" href="?page={{ $i }}">{{ $i }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
|
||||
{{ if lt .CurrentPage .TotalPages }}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ add .CurrentPage 1 }}">Next</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
|
||||
{{ else }}
|
||||
<div class="alert alert-info">No messages found.</div>
|
||||
{{ end }}
|
||||
|
||||
<div class="mt-3">
|
||||
<a href="/account/messages/send" class="btn btn-primary">Compose Message</a>
|
||||
<a href="/account/messages/archived" class="btn btn-outline-secondary ms-2">View Archived</a>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<p class="text-muted">Received: {{ .Message.CreatedAt.Format "02 Jan 2006 15:04" }}</p>
|
||||
<hr>
|
||||
<p>{{ .Message.Message }}</p>
|
||||
<a href="/account/messages" class="btn btn-secondary mt-4">Back to Inbox</a>
|
||||
<a href="/account/messages" class="btn btn-secondary mt-4">Back to Inbox</a> <a href="/account/messages/archive?id={{ .Message.ID }}" class="btn btn-outline-danger mt-3">Archive</a>
|
||||
{{ else }}
|
||||
<div class="alert alert-danger text-center">
|
||||
Message not found or access denied.
|
||||
|
||||
27
templates/account/messages/send.html
Normal file
27
templates/account/messages/send.html
Normal file
@@ -0,0 +1,27 @@
|
||||
{{ define "content" }}
|
||||
<div class="container py-5">
|
||||
<h2>Send a Message</h2>
|
||||
{{ if .Flash }}
|
||||
<div class="alert alert-info">{{ .Flash }}</div>
|
||||
{{ end }}
|
||||
|
||||
<form method="POST" action="/account/messages/send">
|
||||
{{ .CSRFField }}
|
||||
<div class="mb-3">
|
||||
<label for="recipient_id" class="form-label">Recipient User ID</label>
|
||||
<input type="number" class="form-control" name="recipient_id" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="subject" class="form-label">Subject</label>
|
||||
<input type="text" class="form-control" name="subject" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="message" class="form-label">Message</label>
|
||||
<textarea class="form-control" name="message" rows="5" required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Send</button>
|
||||
</form>
|
||||
|
||||
<a href="/account/messages" class="btn btn-secondary mt-3">Back to Inbox</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -100,11 +100,12 @@
|
||||
</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li class="text-center"><a href="/account/messages" class="dropdown-item">View all messages</a></li>
|
||||
{{ else }}
|
||||
<li class="text-center text-muted py-2">No messages</li>
|
||||
{{ end }}
|
||||
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li class="text-center"><a href="/account/messages" class="dropdown-item">View all messages</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user