Files
GoMFT/components/layout.templ
T
StarFleetCPTN 3b9ff3b6a8 chore: Refine config form and UI components
- Add x-init hook to set default form values in config form
- Adjust scrollbar hover color in layout
- Create marker files for providers, source, and destination packages
- Remove unnecessary error styling in local source form
2025-03-08 23:03:41 -08:00

530 lines
20 KiB
Templ

package components
import (
"context"
"github.com/gin-gonic/gin"
)
// 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="light h-full">
<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>
<script src="https://cdn.tailwindcss.com"></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',
},
secondary: {
50: '#f8fafc',
100: '#f1f5f9',
200: '#e2e8f0',
300: '#cbd5e1',
400: '#94a3b8',
500: '#64748b',
600: '#475569',
700: '#334155',
800: '#1e293b',
900: '#0f172a',
950: '#020617',
},
},
fontFamily: {
sans: ['Inter var', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'sans-serif'],
},
boxShadow: {
'custom': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
'custom-lg': '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
},
}
}
}
</script>
<style type="text/tailwindcss">
@layer components {
.btn-primary {
@apply px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-all duration-200 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2;
}
.btn-secondary {
@apply px-4 py-2 bg-secondary-200 text-secondary-800 rounded-lg hover:bg-secondary-300 transition-all duration-200 shadow-sm hover:shadow-md focus:outline-none focus:ring-2 focus:ring-secondary-300 focus:ring-offset-2 dark:bg-secondary-700 dark:text-secondary-100 dark:hover:bg-secondary-600;
}
.btn-danger {
@apply px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-all duration-200 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2;
}
.btn-warning {
@apply px-4 py-2 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 transition-all duration-200 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-yellow-500 focus:ring-offset-2;
}
.form-input {
@apply w-full px-3 py-2 border border-secondary-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-primary-500 shadow-sm transition-all duration-200 dark:bg-secondary-800 dark:border-secondary-700 dark:text-white;
}
.form-checkbox {
@apply rounded border-secondary-300 text-primary-600 shadow-sm focus:border-primary-300 focus:ring focus:ring-primary-200 focus:ring-opacity-50 dark:border-secondary-700 dark:bg-secondary-800;
}
.card {
@apply bg-white dark:bg-secondary-800 rounded-xl shadow-custom overflow-hidden border border-secondary-200 dark:border-secondary-700 transition-all duration-200 hover:shadow-custom-lg;
}
.card-header {
@apply px-6 py-4 border-b border-secondary-200 dark:border-secondary-700 bg-secondary-50 dark:bg-secondary-900;
}
.card-body {
@apply p-6;
}
.badge {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
}
.badge-success {
@apply bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100;
}
.badge-warning {
@apply bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100;
}
.badge-danger {
@apply bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100;
}
.badge-info {
@apply bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-100;
}
.table-container {
@apply overflow-hidden rounded-lg shadow-sm border border-secondary-200 dark:border-secondary-700;
}
.table {
@apply min-w-full divide-y divide-secondary-200 dark:divide-secondary-700;
}
.table thead {
@apply bg-secondary-50 dark:bg-secondary-900;
}
.table th {
@apply px-6 py-3 text-left text-xs font-medium text-secondary-500 uppercase tracking-wider dark:text-secondary-400;
}
.table tbody {
@apply bg-white dark:bg-secondary-800 divide-y divide-secondary-200 dark:divide-secondary-700;
}
.table td {
@apply px-6 py-4 whitespace-nowrap text-sm text-secondary-900 dark:text-secondary-100;
}
.nav-link {
@apply inline-flex items-center px-1 pt-1 text-secondary-500 hover:text-secondary-700 dark:text-secondary-400 dark:hover:text-secondary-200 border-b-2 border-transparent hover:border-primary-500 transition-all duration-200;
}
.nav-link-active {
@apply inline-flex items-center px-1 pt-1 text-primary-600 dark:text-primary-400 border-b-2 border-primary-500;
}
}
/* Dark mode styles */
.dark body {
@apply bg-secondary-900 text-secondary-100;
}
.dark .bg-white {
@apply bg-secondary-800;
}
.dark .text-secondary-800 {
@apply text-secondary-100;
}
.dark .text-secondary-700 {
@apply text-secondary-200;
}
.dark .text-secondary-500 {
@apply text-secondary-300;
}
.dark .hover\:text-secondary-700:hover {
@apply text-secondary-100;
}
.dark .hover\:bg-secondary-100:hover {
@apply bg-secondary-700;
}
.dark .bg-secondary-50 {
@apply bg-secondary-900;
}
.dark .bg-secondary-200 {
@apply bg-secondary-700;
}
.dark .shadow-sm {
@apply shadow-secondary-900;
}
/* Additional dark mode improvements */
.dark .bg-secondary-50 {
@apply bg-secondary-900;
}
.dark .bg-secondary-100 {
@apply bg-secondary-800;
}
.dark .border-secondary-200 {
@apply border-secondary-700;
}
/* Custom animations */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.animate-fadeIn {
animation: fadeIn 0.3s ease-in-out;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: theme('colors.secondary.100');
border-radius: 4px;
}
.dark ::-webkit-scrollbar-track {
background: theme('colors.secondary.800');
}
::-webkit-scrollbar-thumb {
background: theme('colors.secondary.300');
border-radius: 4px;
}
.dark ::-webkit-scrollbar-thumb {
background: theme('colors.secondary.600');
}
::-webkit-scrollbar-thumb:hover {
background: theme('colors.secondary.500');
}
.dark ::-webkit-scrollbar-thumb:hover {
background: theme('colors.secondary.500');
}
</style>
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
</head>
<body class="min-h-screen flex flex-col bg-secondary-50 dark:bg-secondary-900 h-full">
if isLoggedIn(ctx) {
<nav class="bg-white dark:bg-secondary-800 shadow-sm sticky top-0 z-10">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<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 class="hidden sm:ml-6 sm:flex sm:space-x-8">
<a href="/dashboard" class="nav-link">
<i class="fas fa-tachometer-alt mr-1"></i> Dashboard
</a>
<a href="/configs" class="nav-link">
<i class="fas fa-cogs mr-1"></i> Configs
</a>
<a href="/jobs" class="nav-link">
<i class="fas fa-tasks mr-1"></i> Jobs
</a>
<a href="/history" class="nav-link">
<i class="fas fa-history mr-1"></i> History
</a>
if isAdmin(ctx) {
<a href="/admin/users" class="nav-link">
<i class="fas fa-users mr-1"></i> Users
</a>
<a href="/admin/tools" class="nav-link">
<i class="fas fa-tools mr-1"></i> Admin Tools
</a>
}
</div>
</div>
<div class="flex items-center space-x-4">
<!-- Theme Toggle -->
<button
id="theme-toggle"
type="button"
class="text-secondary-500 dark:text-secondary-400 hover:bg-secondary-100 dark:hover:bg-secondary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 rounded-lg text-sm p-2"
onclick="toggleTheme()"
>
<i class="fas fa-sun hidden dark:block"></i>
<i class="fas fa-moon block dark:hidden"></i>
</button>
<!-- Mobile menu button -->
<div class="sm:hidden flex items-center">
<button
id="mobile-menu-button"
class="text-secondary-500 dark:text-secondary-400 hover:bg-secondary-100 dark:hover:bg-secondary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 rounded-lg text-sm p-2"
x-data="{}"
@click="$dispatch('toggle-mobile-menu')"
>
<i class="fas fa-bars text-lg"></i>
</button>
</div>
<!-- User Menu -->
<div x-data="{ open: false }" class="relative">
<button @click="open = !open" class="flex text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
<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>
<div x-show="open"
@click.away="open = false"
class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-secondary-800 dark:ring-secondary-700 animate-fadeIn"
role="menu"
aria-orientation="vertical"
aria-labelledby="user-menu-button"
tabindex="-1">
<div class="px-4 py-2 text-xs text-secondary-500 dark:text-secondary-400 border-b border-secondary-200 dark:border-secondary-700">
Signed in as <span class="font-medium">{ getUserEmail(ctx) }</span>
</div>
<a href="/profile" class="block px-4 py-2 text-sm text-secondary-700 hover:bg-secondary-100 dark:text-secondary-200 dark:hover:bg-secondary-700" role="menuitem">
<i class="fas fa-user-circle mr-2"></i> Profile
</a>
<form method="POST" action="/logout">
<button type="submit" class="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-secondary-100 dark:hover:bg-secondary-700" role="menuitem">
<i class="fas fa-sign-out-alt mr-2"></i> Sign out
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</nav>
<!-- Mobile Navigation Menu -->
<div
x-data="{ open: false }"
x-on:toggle-mobile-menu.window="open = !open"
x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 transform -translate-y-2"
x-transition:enter-end="opacity-100 transform translate-y-0"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 transform translate-y-0"
x-transition:leave-end="opacity-0 transform -translate-y-2"
class="sm:hidden bg-white dark:bg-secondary-800 shadow-md"
>
<div class="px-2 pt-2 pb-3 space-y-1">
<a href="/dashboard" class="block px-3 py-2 rounded-md text-base font-medium text-secondary-700 dark:text-secondary-300 hover:bg-primary-50 dark:hover:bg-secondary-700 hover:text-primary-600 dark:hover:text-primary-400">
<i class="fas fa-tachometer-alt mr-2"></i> Dashboard
</a>
<a href="/configs" class="block px-3 py-2 rounded-md text-base font-medium text-secondary-700 dark:text-secondary-300 hover:bg-primary-50 dark:hover:bg-secondary-700 hover:text-primary-600 dark:hover:text-primary-400">
<i class="fas fa-cogs mr-2"></i> Configs
</a>
<a href="/jobs" class="block px-3 py-2 rounded-md text-base font-medium text-secondary-700 dark:text-secondary-300 hover:bg-primary-50 dark:hover:bg-secondary-700 hover:text-primary-600 dark:hover:text-primary-400">
<i class="fas fa-tasks mr-2"></i> Jobs
</a>
<a href="/history" class="block px-3 py-2 rounded-md text-base font-medium text-secondary-700 dark:text-secondary-300 hover:bg-primary-50 dark:hover:bg-secondary-700 hover:text-primary-600 dark:hover:text-primary-400">
<i class="fas fa-history mr-2"></i> History
</a>
if isAdmin(ctx) {
<a href="/admin/users" class="block px-3 py-2 rounded-md text-base font-medium text-secondary-700 dark:text-secondary-300 hover:bg-primary-50 dark:hover:bg-secondary-700 hover:text-primary-600 dark:hover:text-primary-400">
<i class="fas fa-users mr-2"></i> Users
</a>
<a href="/admin/tools" class="block px-3 py-2 rounded-md text-base font-medium text-secondary-700 dark:text-secondary-300 hover:bg-primary-50 dark:hover:bg-secondary-700 hover:text-primary-600 dark:hover:text-primary-400">
<i class="fas fa-tools mr-2"></i> Admin Tools
</a>
}
<a href="/profile" class="block px-3 py-2 rounded-md text-base font-medium text-secondary-700 dark:text-secondary-300 hover:bg-primary-50 dark:hover:bg-secondary-700 hover:text-primary-600 dark:hover:text-primary-400">
<i class="fas fa-user-circle mr-2"></i> Profile
</a>
<form method="POST" action="/logout">
<button type="submit" class="w-full text-left block px-3 py-2 rounded-md text-base font-medium text-red-600 hover:bg-red-50 dark:hover:bg-red-900 hover:text-red-700 dark:hover:text-red-400">
<i class="fas fa-sign-out-alt mr-2"></i> Sign out
</button>
</form>
</div>
</div>
}
<main class="flex-grow w-full max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8 animate-fadeIn pb-mobile-nav">
<!-- Hidden notification area for HTMX targets -->
<div id="notification-area" class="hidden" hx-swap-oob="true"></div>
{ children... }
</main>
<footer class="bg-white dark:bg-secondary-800 shadow-inner mt-auto w-full">
<div class="max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
<p class="text-center text-sm text-secondary-500 dark:text-secondary-400">
GoMFT &copy; { getCurrentYear() } | Secure File Transfer Solution
</p>
</div>
</footer>
if isLoggedIn(ctx) {
<!-- Mobile Bottom Navigation -->
<div class="sm:hidden fixed bottom-0 left-0 right-0 bg-white dark:bg-secondary-800 border-t border-secondary-200 dark:border-secondary-700 shadow-lg mobile-nav-container z-10">
<div class="grid grid-cols-5 h-16">
<a href="/dashboard" class="flex flex-col items-center justify-center text-secondary-500 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400">
<i class="fas fa-tachometer-alt text-lg"></i>
<span class="text-xs mt-1">Dashboard</span>
</a>
<a href="/configs" class="flex flex-col items-center justify-center text-secondary-500 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400">
<i class="fas fa-cogs text-lg"></i>
<span class="text-xs mt-1">Configs</span>
</a>
<a href="/jobs" class="flex flex-col items-center justify-center text-secondary-500 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400">
<i class="fas fa-tasks text-lg"></i>
<span class="text-xs mt-1">Jobs</span>
</a>
<a href="/history" class="flex flex-col items-center justify-center text-secondary-500 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400">
<i class="fas fa-history text-lg"></i>
<span class="text-xs mt-1">History</span>
</a>
<button
type="button"
class="flex flex-col items-center justify-center text-secondary-500 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400"
x-data="{}"
@click="$dispatch('toggle-mobile-menu')"
>
<i class="fas fa-ellipsis-h text-lg"></i>
<span class="text-xs mt-1">More</span>
</button>
</div>
</div>
}
</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 "2025"
}