Files
StarFleetCPTN e9a005b658 feat: Implement version checking and update notification in dashboard
- Added functionality to fetch the latest GitHub release version.
- Enhanced the dashboard to display current and latest version information.
- Updated the version comparison logic to handle special cases and non-semver formats.
- Improved the dashboard template to link to the latest release on GitHub.
2025-04-11 20:29:47 -07:00

614 lines
28 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;" hx-on::after-swap="initFlowbite()">
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">
<img src="/static/img/logo.png" alt="GoMFT" class="w-8 h-8 mr-2">
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-chart-pie 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-exchange-alt mr-3 text-gray-500 dark:text-gray-400"></i>
Transfer Configurations
</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-calendar-alt mr-3 text-gray-500 dark:text-gray-400"></i>
Scheduled 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>
Transfer 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>
User Management
</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>
Role Management
</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/logs" 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-stream w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
Log Viewer
</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">
<!-- GoMFT Logo -->
<img src="/static/img/logo.png" alt="GoMFT" class="w-8 h-8 mr-2">
</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-chart-pie 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-exchange-alt mr-3 text-gray-500 dark:text-gray-400"></i>
Transfer Configurations
</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-calendar-alt mr-3 text-gray-500 dark:text-gray-400"></i>
Scheduled 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>
Transfer 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>
User Management
</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>
Role Management
</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/logs" 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-stream w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
Log Viewer
</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-mobile" data-collapse-toggle="dropdown-settings-mobile">
<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-mobile" 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-start"
>
<span class="sr-only">View notifications</span>
<i class="fas fa-bell"></i>
<!-- Notification count will be placed here via HTMX -->
</button>
<!-- Place the notification count container outside but still in the relative container -->
<div id="notification-count-container" class="absolute top-0 right-0" hx-get="/notifications/count" hx-trigger="load, notification-updated from:body"></div>
</div>
<!-- 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 fixed"
id="notification-dropdown"
style="min-width: 320px; width: 100%;"
data-dropdown-placement="bottom-start"
>
<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>
<!-- 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 &copy; { 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="https://discord.gg/f9dwtM3j" class="hover:text-primary-600 dark:hover:text-primary-400" target="_blank">
<i class="fab fa-discord"></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 &copy; { 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="https://discord.gg/f9dwtM3j" class="hover:text-primary-600 dark:hover:text-primary-400" target="_blank">
<i class="fab fa-discord"></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>
<!-- Dropdown fix for mobile -->
<script>
document.addEventListener("DOMContentLoaded", function() {
// Initialize the mobile dropdown separately
const mobileDropdownButton = document.querySelector('[data-collapse-toggle="dropdown-settings-mobile"]');
const mobileDropdown = document.getElementById("dropdown-settings-mobile");
if (mobileDropdownButton && mobileDropdown) {
// Show dropdown if on admin pages
if (window.location.pathname.startsWith("/admin")) {
// Keep it hidden by default, will be toggled by button
mobileDropdown.classList.add("hidden");
}
mobileDropdownButton.addEventListener("click", function() {
mobileDropdown.classList.toggle("hidden");
});
}
});
</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")
}