mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-08 15:41:20 +02:00
- Added sorting capabilities to file metadata lists, including SortBy and SortDir fields. - Integrated a toast notification system for user feedback on actions. - Updated templates and JavaScript for improved user interaction and responsiveness. - Refactored file metadata handlers to utilize new sorting features and ensure proper data flow. - Introduced new notification dialog components for better user confirmation on actions.
579 lines
26 KiB
Templ
579 lines
26 KiB
Templ
package components
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"github.com/gin-gonic/gin"
|
|
"time"
|
|
"github.com/starfleetcptn/gomft/components/shared/toast"
|
|
)
|
|
|
|
// AppVersion will be set at build time using ldflags
|
|
// Example build command:
|
|
// go build -ldflags "-X github.com/starfleetcptn/gomft/components.AppVersion=1.2.3"
|
|
var AppVersion = "DEV"
|
|
|
|
// GetReleaseURL returns the URL to the specific GitHub release
|
|
func GetReleaseURL() templ.SafeURL {
|
|
return templ.SafeURL(fmt.Sprintf("https://github.com/starfleetcptn/gomft/releases/tag/%s", AppVersion))
|
|
}
|
|
|
|
// CreateTemplateContext creates a new context with user information from Gin's context
|
|
func CreateTemplateContext(c *gin.Context) context.Context {
|
|
ctx := context.Background()
|
|
if userID, exists := c.Get("userID"); exists {
|
|
ctx = context.WithValue(ctx, "userID", userID)
|
|
}
|
|
if username, exists := c.Get("username"); exists {
|
|
ctx = context.WithValue(ctx, "username", username)
|
|
}
|
|
if email, exists := c.Get("email"); exists {
|
|
ctx = context.WithValue(ctx, "email", email)
|
|
}
|
|
if isAdmin, exists := c.Get("isAdmin"); exists {
|
|
ctx = context.WithValue(ctx, "isAdmin", isAdmin)
|
|
}
|
|
return ctx
|
|
}
|
|
|
|
templ Layout(title string) {
|
|
@LayoutWithContext(title, context.Background())
|
|
}
|
|
|
|
templ LayoutWithContext(title string, ctx context.Context) {
|
|
<!DOCTYPE html>
|
|
<html lang="en" class="h-full bg-gray-50 dark:bg-gray-900">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
|
|
<meta name="theme-color" content="#2563eb"/>
|
|
<meta name="apple-mobile-web-app-capable" content="yes"/>
|
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
|
|
<link rel="apple-touch-icon" sizes="180x180" href="/static/apple-touch-icon.png"/>
|
|
<link rel="icon" type="image/png" sizes="32x32" href="/static/favicon-32x32.png"/>
|
|
<link rel="icon" type="image/png" sizes="16x16" href="/static/favicon-16x16.png"/>
|
|
<link rel="manifest" href="/static/site.webmanifest"/>
|
|
<title>{ title } - GoMFT</title>
|
|
<!-- Font Awesome -->
|
|
<link rel="stylesheet" href="/static/dist/fontawesome/css/all.min.css"/>
|
|
<!-- Application assets -->
|
|
<link rel="stylesheet" href="/static/dist/app.css"/>
|
|
<link rel="stylesheet" href="https://rsms.me/inter/inter.css"/>
|
|
|
|
<script>
|
|
// Immediate theme application
|
|
// This script runs before the DOM is fully loaded
|
|
(function() {
|
|
const isDark = localStorage.theme === 'dark' ||
|
|
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
|
|
if (isDark) {
|
|
document.documentElement.classList.add('dark');
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
document.body.classList.add('dark');
|
|
|
|
// Apply dark theme to containers
|
|
const containers = ['jobs-container', 'configs-container'];
|
|
containers.forEach(function(id) {
|
|
const container = document.getElementById(id);
|
|
if (container) {
|
|
container.classList.add('dark');
|
|
container.style.backgroundColor = '#111827';
|
|
}
|
|
});
|
|
});
|
|
}
|
|
})();
|
|
</script>
|
|
<style>
|
|
html, body {
|
|
margin: 0;
|
|
padding: 0;
|
|
overflow-x: hidden;
|
|
width: 100%;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
.animate-fadeIn {
|
|
animation: fadeIn 0.3s ease-in-out;
|
|
}
|
|
|
|
.pb-mobile-nav {
|
|
padding-bottom: 4rem;
|
|
}
|
|
|
|
@media (min-width: 640px) {
|
|
.pb-mobile-nav {
|
|
padding-bottom: 0;
|
|
}
|
|
}
|
|
|
|
/* Ensure dark mode background extends to full height */
|
|
body.dark {
|
|
background-color: #111827; /* gray-900 */
|
|
}
|
|
|
|
/* Force dark background on specific pages */
|
|
body.dark .configs-page,
|
|
body.dark .jobs-page {
|
|
background-color: #111827 !important; /* gray-900 */
|
|
min-height: 100vh;
|
|
}
|
|
|
|
/* Ensure dark mode fills entire page */
|
|
body.dark {
|
|
background-color: #111827 !important;
|
|
}
|
|
|
|
/* Force container backgrounds */
|
|
#jobs-container.dark, #configs-container.dark {
|
|
background-color: #111827 !important;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
/* Override any white backgrounds in dark mode */
|
|
body.dark .bg-white {
|
|
background-color: #1f2937 !important;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="min-h-full bg-gray-50 dark:bg-gray-900" style="min-height: 100vh; display: flex; flex-direction: column;">
|
|
if isLoggedIn(ctx) {
|
|
<!-- Application Shell -->
|
|
<div class="flex min-h-screen bg-gray-50 dark:bg-gray-900">
|
|
<!-- Desktop Sidebar -->
|
|
<aside class="hidden md:flex md:w-64 md:flex-col md:fixed md:inset-y-0">
|
|
<div class="flex flex-col flex-grow pt-5 bg-white dark:bg-gray-800 overflow-y-auto border-r border-gray-200 dark:border-gray-700">
|
|
<div class="flex items-center flex-shrink-0 px-4">
|
|
<a href="/" class="flex items-center text-xl font-bold text-primary-600 dark:text-primary-400">
|
|
<i class="fas fa-exchange-alt mr-2"></i>
|
|
GoMFT
|
|
</a>
|
|
</div>
|
|
<nav class="flex-1 px-2 py-4 bg-white dark:bg-gray-800">
|
|
<div class="space-y-1">
|
|
<a href="/dashboard" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-tachometer-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
|
Dashboard
|
|
</a>
|
|
<a href="/configs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-cogs mr-3 text-gray-500 dark:text-gray-400"></i>
|
|
Configs
|
|
</a>
|
|
<a href="/jobs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-tasks mr-3 text-gray-500 dark:text-gray-400"></i>
|
|
Jobs
|
|
</a>
|
|
<a href="/history" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-history mr-3 text-gray-500 dark:text-gray-400"></i>
|
|
History
|
|
</a>
|
|
<a href="/files" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-file-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
|
Files
|
|
</a>
|
|
if isAdmin(ctx) {
|
|
<p class="px-3 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
Administration
|
|
</p>
|
|
<a href="/admin/users" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-users w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
|
Users
|
|
</a>
|
|
<a href="/admin/roles" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-user-shield w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
|
Roles
|
|
</a>
|
|
<a href="/admin/audit" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-clipboard-list w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
|
Audit Logs
|
|
</a>
|
|
<a href="/admin/database" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-database w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
|
Database Tools
|
|
</a>
|
|
// Settings Dropdown
|
|
<button type="button" class="flex items-center w-full p-2 text-base text-gray-900 transition duration-75 rounded-lg group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700" aria-controls="dropdown-settings" data-collapse-toggle="dropdown-settings">
|
|
<i class="fas fa-cog w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
|
<span class="flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">Settings</span>
|
|
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
|
|
</svg>
|
|
</button>
|
|
<ul id="dropdown-settings" class="hidden py-2 space-y-2">
|
|
// <li>
|
|
// <a href="#" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">General (Coming Soon)</a>
|
|
// </li>
|
|
<li>
|
|
<a href="/admin/settings/auth-providers" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-user-lock w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
|
Authentication Providers
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="/admin/settings/notifications" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-bell w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
|
Notification Services
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
}
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
</aside>
|
|
<!-- Mobile menu -->
|
|
<div class="md:hidden fixed top-0 z-30 w-full">
|
|
<div class="bg-white dark:bg-gray-800 shadow-sm">
|
|
<div class="flex items-center justify-between px-4 py-3">
|
|
<a href="/" class="flex items-center text-primary-600 dark:text-primary-400">
|
|
<i class="fas fa-exchange-alt text-xl"></i>
|
|
</a>
|
|
<div class="flex items-center space-x-3">
|
|
<button data-drawer-target="mobile-menu" data-drawer-toggle="mobile-menu" class="p-2 text-gray-500 rounded-lg hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-700">
|
|
<i class="fas fa-bars text-lg"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Add this drawer element for mobile menu -->
|
|
<div id="mobile-menu" class="fixed top-0 left-0 z-40 h-screen p-4 overflow-y-auto transition-transform -translate-x-full bg-white w-64 dark:bg-gray-800" tabindex="-1">
|
|
<div class="flex items-center justify-between">
|
|
<a href="/" class="flex items-center text-xl font-bold text-primary-600 dark:text-primary-400">
|
|
<i class="fas fa-exchange-alt mr-2"></i>
|
|
GoMFT
|
|
</a>
|
|
<button type="button" data-drawer-hide="mobile-menu" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 inline-flex items-center dark:hover:bg-gray-600 dark:hover:text-white">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<!-- Copy the same navigation items from the desktop sidebar -->
|
|
<nav class="mt-5">
|
|
<div class="space-y-1">
|
|
<!-- Copy the same links from the desktop sidebar -->
|
|
<a href="/dashboard" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-tachometer-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
|
Dashboard
|
|
</a>
|
|
<a href="/configs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-cogs mr-3 text-gray-500 dark:text-gray-400"></i>
|
|
Configs
|
|
</a>
|
|
<a href="/jobs" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-tasks mr-3 text-gray-500 dark:text-gray-400"></i>
|
|
Jobs
|
|
</a>
|
|
<a href="/history" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-history mr-3 text-gray-500 dark:text-gray-400"></i>
|
|
History
|
|
</a>
|
|
<a href="/files" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-file-alt mr-3 text-gray-500 dark:text-gray-400"></i>
|
|
Files
|
|
</a>
|
|
if isAdmin(ctx) {
|
|
<p class="px-3 text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
Administration
|
|
</p>
|
|
<a href="/admin/users" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-users w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
|
Users
|
|
</a>
|
|
<a href="/admin/roles" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-user-shield w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
|
Roles
|
|
</a>
|
|
<a href="/admin/audit" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-clipboard-list w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
|
Audit Logs
|
|
</a>
|
|
<a href="/admin/database" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-database w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
|
Database Tools
|
|
</a>
|
|
// Settings Dropdown
|
|
<button type="button" class="flex items-center w-full p-2 text-base text-gray-900 transition duration-75 rounded-lg group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700" aria-controls="dropdown-settings" data-collapse-toggle="dropdown-settings">
|
|
<i class="fas fa-cog w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
|
<span class="flex-1 ms-3 text-left rtl:text-right whitespace-nowrap">Settings</span>
|
|
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 10 6">
|
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
|
|
</svg>
|
|
</button>
|
|
<ul id="dropdown-settings" class="hidden py-2 space-y-2">
|
|
// <li>
|
|
// <a href="#" class="flex items-center w-full p-2 text-gray-900 transition duration-75 rounded-lg pl-11 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700">General (Coming Soon)</a>
|
|
// </li>
|
|
<li>
|
|
<a href="/admin/settings/auth-providers" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-user-lock w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
|
Authentication Providers
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<a href="/admin/settings/notifications" class="group flex items-center px-2 py-2 text-sm font-medium rounded-md text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700">
|
|
<i class="fas fa-bell w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
|
Notification Services
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
}
|
|
</div>
|
|
</nav>
|
|
</div>
|
|
<!-- Main Content -->
|
|
<div class="md:pl-64 flex flex-col flex-1 w-full bg-gray-50 dark:bg-gray-900">
|
|
<!-- Main header -->
|
|
<header class="bg-white dark:bg-gray-800 shadow-sm">
|
|
<div class="flex items-center justify-between px-4 py-3 sm:px-6 lg:px-8">
|
|
<div class="flex items-center space-x-3">
|
|
<h1 class="text-lg font-semibold text-gray-900 dark:text-white">{ title }</h1>
|
|
</div>
|
|
<!-- Theme toggle and user menu -->
|
|
<div class="flex items-center space-x-4">
|
|
<!-- Theme Toggle -->
|
|
<button
|
|
id="theme-toggle"
|
|
type="button"
|
|
class="p-2 text-gray-500 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-700 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600"
|
|
onclick="toggleTheme()"
|
|
>
|
|
<i class="fas fa-sun hidden dark:block"></i>
|
|
<i class="fas fa-moon block dark:hidden"></i>
|
|
</button>
|
|
<!-- Notification Bell -->
|
|
<div class="relative">
|
|
<button
|
|
type="button"
|
|
class="p-2 text-gray-500 rounded-lg hover:text-gray-900 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-700 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600 relative"
|
|
id="notification-bell"
|
|
data-dropdown-toggle="notification-dropdown"
|
|
hx-get="/notifications/dropdown"
|
|
hx-trigger="click once"
|
|
hx-target="#notification-dropdown-content"
|
|
data-dropdown-placement="bottom-end"
|
|
>
|
|
<span class="sr-only">View notifications</span>
|
|
<i class="fas fa-bell"></i>
|
|
<!-- Notification badge will be loaded dynamically -->
|
|
<div hx-get="/notifications/count" hx-trigger="load, notification-updated from:body" id="notification-count-container"></div>
|
|
</button>
|
|
<!-- Notification dropdown -->
|
|
<div
|
|
class="hidden overflow-hidden z-50 my-4 max-w-sm md:max-w-md w-full text-base list-none bg-white rounded divide-y divide-gray-100 shadow-lg dark:divide-gray-600 dark:bg-gray-700 rounded-xl"
|
|
id="notification-dropdown"
|
|
style="min-width: 320px; width: 100%;"
|
|
data-dropdown-placement="bottom-end"
|
|
>
|
|
<div id="notification-dropdown-content">
|
|
<!-- Content will be loaded dynamically via HTMX -->
|
|
<div class="block py-2 px-4 text-base font-medium text-center text-gray-700 bg-gray-50 dark:bg-gray-600 dark:text-gray-300">
|
|
Notifications
|
|
</div>
|
|
<div class="py-4 px-4 text-center text-gray-500 dark:text-gray-400">
|
|
<div class="animate-pulse flex flex-col items-center">
|
|
<div class="rounded-full bg-gray-200 dark:bg-gray-700 h-12 w-12 mb-2"></div>
|
|
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-4"></div>
|
|
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-full mb-2"></div>
|
|
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-full mb-2"></div>
|
|
<div class="h-2 bg-gray-200 dark:bg-gray-700 rounded w-3/4"></div>
|
|
</div>
|
|
</div>
|
|
<a href="/notifications" class="block py-2 text-md font-medium text-center text-gray-900 bg-gray-50 hover:bg-gray-100 dark:bg-gray-600 dark:text-white dark:hover:underline">
|
|
<div class="inline-flex items-center">
|
|
<svg aria-hidden="true" class="mr-2 w-4 h-4 text-gray-500 dark:text-gray-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
|
|
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"></path>
|
|
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"></path>
|
|
</svg>
|
|
View all
|
|
</div>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- User menu dropdown -->
|
|
<div class="relative">
|
|
<button
|
|
type="button"
|
|
class="flex text-sm rounded-full focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600"
|
|
id="user-menu-button"
|
|
data-dropdown-toggle="user-dropdown"
|
|
data-dropdown-placement="bottom-end"
|
|
>
|
|
<span class="sr-only">Open user menu</span>
|
|
<div class="h-8 w-8 rounded-full bg-primary-100 text-primary-700 flex items-center justify-center dark:bg-primary-900 dark:text-primary-300">
|
|
<span class="text-sm font-medium">{ getUserInitial(ctx) }</span>
|
|
</div>
|
|
</button>
|
|
<!-- Dropdown menu -->
|
|
<div
|
|
class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600"
|
|
id="user-dropdown"
|
|
>
|
|
<div class="px-4 py-3">
|
|
<span class="block text-sm text-gray-900 dark:text-white">{ getUserEmail(ctx) }</span>
|
|
</div>
|
|
<ul class="py-2" aria-labelledby="user-menu-button">
|
|
<li>
|
|
<a
|
|
href="/profile"
|
|
class="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white"
|
|
>
|
|
<i class="fas fa-user-circle mr-2 w-4"></i>
|
|
Profile
|
|
</a>
|
|
</li>
|
|
<li>
|
|
<form method="POST" action="/logout">
|
|
<button
|
|
type="submit"
|
|
class="w-full text-left flex items-center px-4 py-2 text-sm text-red-600 hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
|
>
|
|
<i class="fas fa-sign-out-alt mr-2 w-4"></i>
|
|
Sign out
|
|
</button>
|
|
</form>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
<!-- Toast Container -->
|
|
@toast.Container()
|
|
<!-- Page Content -->
|
|
<main class="flex-1 bg-gray-50 dark:bg-gray-900">
|
|
<div class="py-6 bg-gray-50 dark:bg-gray-900">
|
|
<div class="max-w-screen-2xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<!-- Notification area -->
|
|
<div id="notification-area" class="hidden" hx-swap-oob="true"></div>
|
|
{ children... }
|
|
</div>
|
|
</div>
|
|
</main>
|
|
<!-- Footer -->
|
|
<footer class="bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700">
|
|
<div class="max-w-screen-2xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
|
|
<div class="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
|
|
<span>GoMFT © { getCurrentYear() }</span>
|
|
<div class="flex items-center space-x-4">
|
|
<a href="https://github.com/starfleetcptn/gomft" class="hover:text-primary-600 dark:hover:text-primary-400">
|
|
<i class="fab fa-github"></i>
|
|
</a>
|
|
<a href={ GetReleaseURL() } class="hover:text-primary-600 dark:hover:text-primary-400">
|
|
<i class="fas fa-tag"></i> { AppVersion }
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
} else {
|
|
<!-- Public page layout -->
|
|
<div class="flex flex-col min-h-screen bg-gray-50 dark:bg-gray-900">
|
|
<main class="flex-grow">
|
|
{ children... }
|
|
</main>
|
|
<footer class="bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700">
|
|
<div class="max-w-screen-2xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
|
|
<div class="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
|
|
<span>GoMFT © { getCurrentYear() }</span>
|
|
<div class="flex items-center space-x-4">
|
|
<a href="https://github.com/starfleetcptn/gomft" class="hover:text-primary-600 dark:hover:text-primary-400">
|
|
<i class="fab fa-github"></i>
|
|
</a>
|
|
<a href={ GetReleaseURL() } class="hover:text-primary-600 dark:hover:text-primary-400">
|
|
<i class="fas fa-tag"></i> { AppVersion }
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
}
|
|
<!-- Scripts -->
|
|
<!-- Alpine.js and dependencies -->
|
|
<script defer src="/static/dist/vendor.js"></script>
|
|
<!-- Shared Scripts -->
|
|
@toast.ShowToastJS()
|
|
<!-- Application scripts -->
|
|
<script defer src="/static/dist/app.js"></script>
|
|
<script defer src="/static/dist/init.js"></script>
|
|
</body>
|
|
</html>
|
|
}
|
|
|
|
// Helper function to check if user is admin
|
|
func isAdmin(ctx context.Context) bool {
|
|
// Try as bool first
|
|
if admin, ok := ctx.Value("isAdmin").(bool); ok {
|
|
return admin
|
|
}
|
|
// Try as interface{} (from JWT claims)
|
|
if admin, ok := ctx.Value("isAdmin").(interface{}); ok {
|
|
if boolVal, ok := admin.(bool); ok {
|
|
return boolVal
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Helper function to check if user is logged in
|
|
func isLoggedIn(ctx context.Context) bool {
|
|
// First try as uint
|
|
if userID, ok := ctx.Value("userID").(uint); ok && userID > 0 {
|
|
return true
|
|
}
|
|
// Then try as float64 (from JWT claims)
|
|
if userID, ok := ctx.Value("userID").(float64); ok && userID > 0 {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Helper function to get user initial for avatar
|
|
func getUserInitial(ctx context.Context) string {
|
|
// Try as string first
|
|
if username, ok := ctx.Value("username").(string); ok && username != "" {
|
|
return strings.ToUpper(string(username[0]))
|
|
}
|
|
// Try as interface{} (from JWT claims)
|
|
if username, ok := ctx.Value("username").(interface{}); ok {
|
|
if strVal, ok := username.(string); ok && strVal != "" {
|
|
return strings.ToUpper(string(strVal[0]))
|
|
}
|
|
}
|
|
// Try email as fallback
|
|
if email, ok := ctx.Value("email").(string); ok && email != "" {
|
|
return strings.ToUpper(string(email[0]))
|
|
}
|
|
return "U"
|
|
}
|
|
|
|
// Helper function to get user email
|
|
func getUserEmail(ctx context.Context) string {
|
|
// Try as string first
|
|
if email, ok := ctx.Value("email").(string); ok && email != "" {
|
|
return strings.ToLower(email)
|
|
}
|
|
// Try as interface{} (from JWT claims)
|
|
if email, ok := ctx.Value("email").(interface{}); ok {
|
|
if strVal, ok := email.(string); ok && strVal != "" {
|
|
return strings.ToLower(strVal)
|
|
}
|
|
}
|
|
return "user@example.com"
|
|
}
|
|
|
|
// Helper function to get current year
|
|
func getCurrentYear() string {
|
|
return time.Now().Format("2006")
|
|
}
|