mirror of
https://github.com/StarFleetCPTN/GoMFT.git
synced 2026-09-08 23:50:48 +02:00
- Implemented pagination for user notifications, allowing users to navigate through their notifications more efficiently. - Updated the notifications page to display the total count of notifications and provide controls for adjusting the number of notifications shown per page. - Added backend support for fetching paginated notifications and counting total notifications for users. - Improved the user interface for notifications, including a new layout and additional help sections to guide users on managing their notifications.
567 lines
22 KiB
Templ
567 lines
22 KiB
Templ
package components
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"github.com/gin-gonic/gin"
|
|
"time"
|
|
)
|
|
|
|
// 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>
|
|
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
|
<script src="https://unpkg.com/alpinejs@3.13.5/dist/cdn.min.js" defer></script>
|
|
<!-- Tailwind CSS with Flowbite -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.2.1/flowbite.min.css" rel="stylesheet"/>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.2.1/flowbite.min.js"></script>
|
|
<script src="/static/js/app.js"></script>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"/>
|
|
<link rel="stylesheet" href="/static/css/app.css"/>
|
|
<!-- Notyf Toast Notifications -->
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/notyf@3/notyf.min.css"/>
|
|
<script src="https://cdn.jsdelivr.net/npm/notyf@3/notyf.min.js"></script>
|
|
<script>
|
|
// Initialize Notyf and make it available globally
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
window.notyfInstance = new Notyf({
|
|
duration: 3000,
|
|
position: {
|
|
x: 'right',
|
|
y: 'bottom',
|
|
},
|
|
types: [
|
|
{
|
|
type: 'success',
|
|
className: 'notyf__toast--success',
|
|
background: '#10B981',
|
|
icon: {
|
|
className: 'fas fa-check-circle',
|
|
tagName: 'i'
|
|
}
|
|
},
|
|
{
|
|
type: 'error',
|
|
className: 'notyf__toast--error',
|
|
background: '#EF4444',
|
|
icon: {
|
|
className: 'fas fa-exclamation-circle',
|
|
tagName: 'i'
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
});
|
|
</script>
|
|
<script>
|
|
tailwind.config = {
|
|
darkMode: 'class',
|
|
theme: {
|
|
extend: {
|
|
colors: {
|
|
primary: {
|
|
50: '#f0f9ff',
|
|
100: '#e0f2fe',
|
|
200: '#bae6fd',
|
|
300: '#7dd3fc',
|
|
400: '#38bdf8',
|
|
500: '#0ea5e9',
|
|
600: '#0284c7',
|
|
700: '#0369a1',
|
|
800: '#075985',
|
|
900: '#0c4a6e',
|
|
950: '#082f49',
|
|
}
|
|
},
|
|
fontFamily: {
|
|
sans: ['Inter var', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'sans-serif'],
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</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>
|
|
<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>
|
|
</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>
|
|
<a href="/admin/settings" 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-cog w-4 h-4 mr-2 text-gray-500 dark:text-gray-400"></i>
|
|
Settings
|
|
</a>
|
|
}
|
|
</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>
|
|
<!-- User menu (same as desktop) -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</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">
|
|
<button data-drawer-target="desktop-menu" data-drawer-toggle="desktop-menu" class="md:hidden 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>
|
|
<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>
|
|
<!-- 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>
|
|
}
|
|
<script>
|
|
// Add script to ensure dark mode is properly applied
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Apply body dark class when theme changes
|
|
const isDark = document.documentElement.classList.contains('dark');
|
|
if (isDark) {
|
|
document.body.classList.add('dark');
|
|
|
|
// Also apply to containers
|
|
const jobsContainer = document.getElementById('jobs-container');
|
|
const configsContainer = document.getElementById('configs-container');
|
|
|
|
if (jobsContainer) jobsContainer.classList.add('dark');
|
|
if (configsContainer) configsContainer.classList.add('dark');
|
|
}
|
|
|
|
// Initialize admin dropdown toggle if available
|
|
const adminDropdownToggle = document.querySelector('[data-collapse-toggle="dropdown-admin"]');
|
|
const adminDropdown = document.getElementById('dropdown-admin');
|
|
|
|
if (adminDropdownToggle && adminDropdown) {
|
|
// Check if we should show the dropdown (if current page is under admin section)
|
|
const currentPath = window.location.pathname;
|
|
if (currentPath.startsWith('/admin')) {
|
|
adminDropdown.classList.remove('hidden');
|
|
}
|
|
|
|
// Add click event listener
|
|
adminDropdownToggle.addEventListener('click', function() {
|
|
adminDropdown.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 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 string(strVal[0])
|
|
}
|
|
}
|
|
// Try email as fallback
|
|
if email, ok := ctx.Value("email").(string); ok && email != "" {
|
|
return 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 email
|
|
}
|
|
// Try as interface{} (from JWT claims)
|
|
if email, ok := ctx.Value("email").(interface{}); ok {
|
|
if strVal, ok := email.(string); ok && strVal != "" {
|
|
return strVal
|
|
}
|
|
}
|
|
return "user@example.com"
|
|
}
|
|
|
|
// Helper function to get current year
|
|
func getCurrentYear() string {
|
|
return time.Now().Format("2006")
|
|
}
|