mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-08 15:41:20 +02:00
- Introduced new templates for displaying and managing user notifications, including pagination support. - Implemented helper functions for formatting notification times and determining icon and background colors based on notification types. - Enhanced the user interface with controls for marking notifications as read and adjusting the number of notifications displayed per page. - Added a dropdown for quick access to notifications and a count badge for unread notifications. - Included a help section to guide users on managing their notifications effectively.
315 lines
16 KiB
Templ
315 lines
16 KiB
Templ
package components
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strconv"
|
|
)
|
|
|
|
// Note: To implement pagination, update the NotificationsData struct in notifications.templ
|
|
// to include these fields:
|
|
// CurrentPage int
|
|
// TotalPages int
|
|
// TotalCount int
|
|
// PerPage int
|
|
|
|
templ NotificationsPage(ctx context.Context, data NotificationsData) {
|
|
@LayoutWithContext("Notifications", ctx) {
|
|
<div class="py-6 px-4 mx-auto max-w-7xl lg:px-8">
|
|
<!-- Page Header -->
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between mb-8">
|
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
|
|
<i class="fas fa-bell w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
|
|
Notifications
|
|
</h1>
|
|
<div class="text-sm text-gray-500 dark:text-gray-400 bg-gray-50 dark:bg-gray-800 rounded-lg px-4 py-2 inline-flex items-center">
|
|
<i class="fas fa-info-circle mr-2"></i>
|
|
<span class="font-medium">Total: { fmt.Sprint(data.TotalCount) } notifications</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Controls and Filters -->
|
|
<div class="mb-6 p-4 bg-white border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
|
|
<div class="flex flex-col sm:flex-row sm:items-center gap-4">
|
|
<div class="ml-auto">
|
|
<label class="inline-flex items-center text-sm text-gray-700 dark:text-gray-300">
|
|
Show
|
|
<select
|
|
id="perPage"
|
|
name="perPage"
|
|
class="mx-2 bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
|
|
hx-get={ fmt.Sprintf("/notifications?page=1&perPage=%s", "${this.value}") }
|
|
hx-target="body"
|
|
hx-swap="innerHTML"
|
|
hx-trigger="change"
|
|
>
|
|
<option value="10" selected?={ data.PerPage == 10 }>10</option>
|
|
<option value="25" selected?={ data.PerPage == 25 }>25</option>
|
|
<option value="50" selected?={ data.PerPage == 50 }>50</option>
|
|
<option value="100" selected?={ data.PerPage == 100 }>100</option>
|
|
</select>
|
|
entries
|
|
</label>
|
|
</div>
|
|
<a
|
|
href="/notifications/mark-all-read"
|
|
hx-post="/notifications/mark-all-read"
|
|
hx-swap="none"
|
|
hx-target="#notification-bell"
|
|
class="inline-flex items-center py-2.5 px-4 text-sm font-medium text-white bg-blue-700 rounded-lg border border-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
|
>
|
|
<i class="fas fa-check-double w-4 h-4 mr-2"></i>
|
|
Mark all as read
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
if len(data.Notifications) == 0 {
|
|
<div class="p-8 text-center bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700">
|
|
<div class="flex justify-center mb-4">
|
|
<span class="inline-flex items-center justify-center w-16 h-16 rounded-full bg-gray-100 dark:bg-gray-700">
|
|
<i class="fas fa-bell-slash text-gray-400 dark:text-gray-500 text-3xl"></i>
|
|
</span>
|
|
</div>
|
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">No notifications</h3>
|
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">You don't have any notifications yet.</p>
|
|
</div>
|
|
} else {
|
|
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 overflow-hidden">
|
|
<ul class="divide-y divide-gray-200 dark:divide-gray-700">
|
|
for _, notification := range data.Notifications {
|
|
<li id={ "notification-" + fmt.Sprintf("%d", notification.ID) }
|
|
class={ templ.KV("bg-blue-50 dark:bg-blue-900/20", !notification.IsRead) }>
|
|
<div class="block transition-colors px-4 py-4 sm:px-6">
|
|
<div class="flex items-start">
|
|
<div class="flex-shrink-0 mr-4">
|
|
<div class={ "w-12 h-12 rounded-full flex items-center justify-center", GetNotificationBgColor(notification.Type) }>
|
|
<i class={ GetNotificationIcon(notification.Type) }></i>
|
|
</div>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<div class="flex justify-between items-start">
|
|
<div>
|
|
<h3 class="text-base font-medium text-gray-900 dark:text-white">{ notification.Title }</h3>
|
|
<p class="text-sm text-gray-500 dark:text-gray-400 mt-1">{ notification.Message }</p>
|
|
</div>
|
|
<div class="flex items-center text-xs text-gray-500 dark:text-gray-400">
|
|
<span>{ FormatNotificationTime(notification.CreatedAt) }</span>
|
|
if !notification.IsRead {
|
|
<button
|
|
hx-post={ "/notifications/" + fmt.Sprintf("%d", notification.ID) + "/read" }
|
|
hx-target={ "#notification-" + fmt.Sprintf("%d", notification.ID) }
|
|
hx-swap="outerHTML"
|
|
class="ml-4 text-blue-700 bg-blue-100 hover:bg-blue-200 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:text-blue-400 dark:bg-blue-900/30 dark:hover:bg-blue-900/40 dark:focus:ring-blue-800"
|
|
>
|
|
<i class="fas fa-check w-3.5 h-3.5 mr-1.5"></i>
|
|
Mark as read
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="mt-2">
|
|
<a
|
|
href={ templ.SafeURL(notification.Link) }
|
|
class="text-blue-700 bg-blue-100 hover:bg-blue-200 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:text-blue-400 dark:bg-blue-900/30 dark:hover:bg-blue-900/40 dark:focus:ring-blue-800">
|
|
<i class="fas fa-eye w-3.5 h-3.5 mr-1.5"></i>
|
|
View details
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
}
|
|
</ul>
|
|
|
|
<!-- Pagination -->
|
|
if data.TotalPages > 1 {
|
|
<div class="mt-6 flex flex-col sm:flex-row justify-between items-center py-4 px-4 border-t border-gray-200 dark:border-gray-700">
|
|
<!-- Mobile pagination -->
|
|
<div class="flex-1 flex justify-between gap-4 sm:hidden w-full mb-4 sm:mb-0">
|
|
if data.CurrentPage > 1 {
|
|
<a
|
|
href={ templ.SafeURL("/notifications?page=" + strconv.Itoa(data.CurrentPage-1) + "&perPage=" + strconv.Itoa(data.PerPage)) }
|
|
hx-get={ "/notifications?page=" + strconv.Itoa(data.CurrentPage-1) + "&perPage=" + strconv.Itoa(data.PerPage) }
|
|
hx-target="body"
|
|
hx-swap="innerHTML"
|
|
class="w-full inline-flex justify-center items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700"
|
|
>
|
|
<i class="fas fa-chevron-left mr-1"></i> Previous
|
|
</a>
|
|
} else {
|
|
<span class="w-full inline-flex justify-center items-center px-4 py-2 text-sm font-medium text-gray-400 bg-gray-100 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-600 dark:text-gray-600 cursor-not-allowed">
|
|
<i class="fas fa-chevron-left mr-1"></i> Previous
|
|
</span>
|
|
}
|
|
|
|
if data.CurrentPage < data.TotalPages {
|
|
<a
|
|
href={ templ.SafeURL("/notifications?page=" + strconv.Itoa(data.CurrentPage+1) + "&perPage=" + strconv.Itoa(data.PerPage)) }
|
|
hx-get={ "/notifications?page=" + strconv.Itoa(data.CurrentPage+1) + "&perPage=" + strconv.Itoa(data.PerPage) }
|
|
hx-target="body"
|
|
hx-swap="innerHTML"
|
|
class="w-full inline-flex justify-center items-center px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700"
|
|
>
|
|
Next <i class="fas fa-chevron-right ml-1"></i>
|
|
</a>
|
|
} else {
|
|
<span class="w-full inline-flex justify-center items-center px-4 py-2 text-sm font-medium text-gray-400 bg-gray-100 border border-gray-300 rounded-lg dark:bg-gray-800 dark:border-gray-600 dark:text-gray-600 cursor-not-allowed">
|
|
Next <i class="fas fa-chevron-right ml-1"></i>
|
|
</span>
|
|
}
|
|
</div>
|
|
|
|
<!-- Desktop pagination -->
|
|
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
|
<div>
|
|
<p class="text-sm text-gray-700 dark:text-gray-300">
|
|
Showing
|
|
<span class="font-medium">{ fmt.Sprint((data.CurrentPage-1)*data.PerPage + 1) }</span>
|
|
to
|
|
<span class="font-medium">{ fmt.Sprint(minInt((data.CurrentPage)*data.PerPage, data.TotalCount)) }</span>
|
|
of
|
|
<span class="font-medium">{ fmt.Sprint(data.TotalCount) }</span>
|
|
notifications
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<nav class="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
|
<!-- First page -->
|
|
<a
|
|
href={ templ.SafeURL("/notifications?page=1&perPage=" + strconv.Itoa(data.PerPage)) }
|
|
hx-get={ "/notifications?page=1&perPage=" + strconv.Itoa(data.PerPage) }
|
|
hx-target="body"
|
|
hx-swap="innerHTML"
|
|
class={ "relative inline-flex items-center justify-center rounded-l-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700", templ.KV("opacity-50 pointer-events-none", data.CurrentPage == 1) }
|
|
>
|
|
<span class="sr-only">First</span>
|
|
<i class="fas fa-angle-double-left text-xs"></i>
|
|
</a>
|
|
|
|
<!-- Previous page -->
|
|
<a
|
|
href={ templ.SafeURL("/notifications?page=" + strconv.Itoa(maxInt(1, data.CurrentPage-1)) + "&perPage=" + strconv.Itoa(data.PerPage)) }
|
|
hx-get={ "/notifications?page=" + strconv.Itoa(maxInt(1, data.CurrentPage-1)) + "&perPage=" + strconv.Itoa(data.PerPage) }
|
|
hx-target="body"
|
|
hx-swap="innerHTML"
|
|
class={ "relative inline-flex items-center justify-center border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700", templ.KV("opacity-50 pointer-events-none", data.CurrentPage == 1) }
|
|
>
|
|
<span class="sr-only">Previous</span>
|
|
<i class="fas fa-angle-left text-xs"></i>
|
|
</a>
|
|
|
|
<!-- Page numbers -->
|
|
@notificationPageNumbers(data.CurrentPage, data.TotalPages, data.PerPage)
|
|
|
|
<!-- Next page -->
|
|
<a
|
|
href={ templ.SafeURL("/notifications?page=" + strconv.Itoa(minInt(data.TotalPages, data.CurrentPage+1)) + "&perPage=" + strconv.Itoa(data.PerPage)) }
|
|
hx-get={ "/notifications?page=" + strconv.Itoa(minInt(data.TotalPages, data.CurrentPage+1)) + "&perPage=" + strconv.Itoa(data.PerPage) }
|
|
hx-target="body"
|
|
hx-swap="innerHTML"
|
|
class={ "relative inline-flex items-center justify-center border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700", templ.KV("opacity-50 pointer-events-none", data.CurrentPage == data.TotalPages) }
|
|
>
|
|
<span class="sr-only">Next</span>
|
|
<i class="fas fa-angle-right text-xs"></i>
|
|
</a>
|
|
|
|
<!-- Last page -->
|
|
<a
|
|
href={ templ.SafeURL("/notifications?page=" + strconv.Itoa(data.TotalPages) + "&perPage=" + strconv.Itoa(data.PerPage)) }
|
|
hx-get={ "/notifications?page=" + strconv.Itoa(data.TotalPages) + "&perPage=" + strconv.Itoa(data.PerPage) }
|
|
hx-target="body"
|
|
hx-swap="innerHTML"
|
|
class={ "relative inline-flex items-center justify-center rounded-r-lg border border-gray-300 bg-white px-3 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700", templ.KV("opacity-50 pointer-events-none", data.CurrentPage == data.TotalPages) }
|
|
>
|
|
<span class="sr-only">Last</span>
|
|
<i class="fas fa-angle-double-right text-xs"></i>
|
|
</a>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<!-- Help Section -->
|
|
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg shadow-sm mt-8 p-4 border border-gray-200 dark:border-gray-700">
|
|
<div class="flex items-start mb-2">
|
|
<div class="flex items-center h-5">
|
|
<i class="fas fa-info-circle w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
|
|
</div>
|
|
<div class="ml-2 text-sm">
|
|
<p class="text-gray-700 dark:text-gray-300">Notifications keep you updated on important system events and alerts.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-start mt-4">
|
|
<div class="flex items-center h-5">
|
|
<i class="fas fa-bell w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
|
|
</div>
|
|
<div class="ml-2 text-sm">
|
|
<p class="text-gray-700 dark:text-gray-300">Unread notifications are highlighted in blue. Mark them as read to clear the highlight.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-start mt-4">
|
|
<div class="flex items-center h-5">
|
|
<i class="fas fa-list-ol w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
|
|
</div>
|
|
<div class="ml-2 text-sm">
|
|
<p class="text-gray-700 dark:text-gray-300">Adjust how many notifications to display per page using the dropdown menu.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
// Helper function for pagination math
|
|
func minInt(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// Helper function for pagination math
|
|
func maxInt(a, b int) int {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// Add the pageNumbers template for consistent pagination layout
|
|
templ notificationPageNumbers(currentPage int, totalPages int, perPage int) {
|
|
// Show at most 5 page numbers with the current page in the middle when possible
|
|
for i := 1; i <= totalPages; i++ {
|
|
if i == currentPage {
|
|
<span aria-current="page" class="relative z-10 inline-flex items-center bg-blue-600 px-4 py-2 text-sm font-medium text-white focus:z-20 border border-blue-600 dark:bg-blue-600">
|
|
{ fmt.Sprint(i) }
|
|
</span>
|
|
} else if i == 1 || i == totalPages || (i >= currentPage-2 && i <= currentPage+2) {
|
|
<a
|
|
href={ templ.SafeURL("/notifications?page=" + strconv.Itoa(i) + "&perPage=" + strconv.Itoa(perPage)) }
|
|
hx-get={ "/notifications?page=" + strconv.Itoa(i) + "&perPage=" + strconv.Itoa(perPage) }
|
|
hx-target="body"
|
|
hx-swap="innerHTML"
|
|
hx-indicator={ fmt.Sprintf("#page-indicator-%d", i) }
|
|
class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-500 hover:bg-gray-50 focus:z-20 border border-gray-300 bg-white dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:bg-gray-700 relative"
|
|
>
|
|
{ fmt.Sprint(i) }
|
|
<span id={ fmt.Sprintf("page-indicator-%d", i) } class="htmx-indicator absolute top-0 right-0 w-4 h-4 -mt-1 -mr-1">
|
|
<i class="fas fa-spinner fa-spin text-xs"></i>
|
|
</span>
|
|
</a>
|
|
} else if i == currentPage-3 || i == currentPage+3 {
|
|
<span class="relative inline-flex items-center px-4 py-2 text-sm font-medium text-gray-700 border border-gray-300 bg-white dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400">
|
|
...
|
|
</span>
|
|
}
|
|
}
|
|
} |