feat: Implement frontend asset build process and update Docker configuration

- Added a new build script using esbuild to bundle JavaScript and CSS assets, including Tailwind CSS and Font Awesome.
- Updated the Dockerfile to include a multi-stage build process for frontend assets, ensuring efficient image creation.
- Enhanced GitHub workflows to automate the installation of Node.js dependencies and build frontend assets during CI/CD.
- Introduced a .gitignore entry for the dist directory to prevent unnecessary files from being tracked.
- Improved caching and content type handling for static files served by the application.
This commit is contained in:
StarFleetCPTN
2025-03-27 15:28:40 -07:00
parent 405b2e605e
commit fde60b7d68
30 changed files with 2556 additions and 241 deletions
@@ -29,6 +29,23 @@ jobs:
with:
fetch-depth: 0 # Needed to get all tags for versioning
# Set up Node.js for frontend build
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
# Install dependencies
- name: Install dependencies
run: npm ci
# Build frontend assets
- name: Build frontend assets
run: |
node build.js
ls -la static/dist/
# Set version information
- name: Set Version
id: version
+17
View File
@@ -30,6 +30,23 @@ jobs:
with:
fetch-depth: 0 # Needed to get all tags for versioning
# Set up Node.js for frontend build
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
# Install dependencies
- name: Install dependencies
run: npm ci
# Build frontend assets
- name: Build frontend assets
run: |
node build.js
ls -la static/dist/
# Set version information
- name: Set Version
id: version
+20
View File
@@ -27,6 +27,26 @@ jobs:
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install Node.js dependencies
run: npm ci
- name: Build frontend assets
run: |
# Build JavaScript and CSS assets
node build.js
# Ensure the dist directory exists
mkdir -p static/dist
# Verify the build output
ls -la static/dist
- name: Set up Go
uses: actions/setup-go@v5
with:
+2
View File
@@ -79,6 +79,8 @@ backups/
/destination/
/archive/
# Ignore the dist directory
static/dist/
# Ignore binaries
gomft
+44 -3
View File
@@ -1,3 +1,32 @@
# Build frontend assets
FROM node:20-alpine AS frontend-builder
WORKDIR /app
# Copy all files needed for the build first
COPY package.json package-lock.json ./
COPY build.js ./
COPY static/ ./static/
COPY tailwind.config.js ./
# Debug: Show the contents of build.js
RUN echo "Contents of build.js:" && cat build.js
# Remove the build script from postinstall
RUN sed -i 's/"postinstall": "npm run build",//' package.json
# Install dependencies and build with verbose output
RUN npm ci && \
echo "Building frontend assets..." && \
node build.js && \
echo "Build complete. Contents of dist:" && \
ls -la static/dist/ && \
echo "Sample of app.js:" && \
head -n 10 static/dist/app.js && \
echo "Sample of app.css:" && \
head -n 10 static/dist/app.css
# Go build stage
FROM golang:1.24-alpine AS builder
WORKDIR /app
@@ -17,7 +46,20 @@ RUN go install github.com/a-h/templ/cmd/templ@latest
COPY go.mod go.sum ./
RUN go mod download
# Copy the rest of the source code
# Create static directory structure
RUN mkdir -p /app/static/dist
# Copy built frontend assets from frontend-builder BEFORE copying Go source
COPY --from=frontend-builder /app/static/dist/ /app/static/dist/
# Copy the rest of the static files
COPY static/ /app/static/
# Verify static files are in place before Go build
RUN echo "Verifying static files before Go build:" && \
ls -la /app/static/dist/
# Now copy the rest of the source code
COPY . .
# Generate template files from .templ files
@@ -61,8 +103,7 @@ RUN addgroup -g ${GID} ${USERNAME} && \
COPY --from=builder /app/gomft /app/
COPY --from=builder /usr/local/bin/rclone /usr/local/bin/rclone
# Copy static files and configurations
COPY static/ /app/static/
# Copy components
COPY components/ /app/components/
# Copy entrypoint script
+122
View File
@@ -0,0 +1,122 @@
import * as esbuild from 'esbuild';
import path from 'path';
import { fileURLToPath } from 'url';
import { execSync } from 'child_process';
import fs from 'fs';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const isWatch = process.argv.includes('--watch');
// Ensure the dist directory exists
const distDir = path.join(__dirname, 'static', 'dist');
if (!fs.existsSync(distDir)) {
fs.mkdirSync(distDir, { recursive: true });
}
// Copy Font Awesome files
const fontAwesomeSrcDir = path.join(__dirname, 'node_modules', '@fortawesome', 'fontawesome-free');
const fontAwesomeDestDir = path.join(distDir, 'fontawesome');
// Copy CSS files
const cssFiles = [
'css/all.min.css',
'css/fontawesome.min.css',
'css/solid.min.css',
'css/regular.min.css',
'css/brands.min.css'
];
cssFiles.forEach(file => {
const srcFile = path.join(fontAwesomeSrcDir, file);
const destFile = path.join(fontAwesomeDestDir, file);
const destDir = path.dirname(destFile);
if (!fs.existsSync(destDir)) {
fs.mkdirSync(destDir, { recursive: true });
}
if (fs.existsSync(srcFile)) {
fs.copyFileSync(srcFile, destFile);
}
});
// Copy webfonts
const webfontsSrcDir = path.join(fontAwesomeSrcDir, 'webfonts');
const webfontsDestDir = path.join(fontAwesomeDestDir, 'webfonts');
if (!fs.existsSync(webfontsDestDir)) {
fs.mkdirSync(webfontsDestDir, { recursive: true });
}
fs.readdirSync(webfontsSrcDir).forEach(file => {
fs.copyFileSync(
path.join(webfontsSrcDir, file),
path.join(webfontsDestDir, file)
);
});
const commonConfig = {
sourcemap: true,
minify: true,
bundle: true,
platform: 'browser',
target: ['es2020'],
};
async function buildTailwind() {
console.log('Building Tailwind CSS...');
execSync('npx tailwindcss -i ./static/css/app.css -o ./static/dist/app.css --minify');
}
async function build() {
try {
// Build vendor JavaScript bundle (CDN dependencies)
await esbuild.build({
...commonConfig,
entryPoints: ['static/js/vendor.js'],
outfile: 'static/dist/vendor.js',
format: 'iife',
});
// Build application JavaScript
await esbuild.build({
...commonConfig,
entryPoints: ['static/js/app.js'],
outfile: 'static/dist/app.js',
format: 'iife',
});
// Build initialization JavaScript
await esbuild.build({
...commonConfig,
entryPoints: ['static/js/init.js'],
outfile: 'static/dist/init.js',
format: 'iife',
});
// Build CSS with Tailwind
await buildTailwind();
console.log('Build completed successfully!');
} catch (error) {
console.error('Build failed:', error);
process.exit(1);
}
}
if (isWatch) {
// Watch mode
console.log('Starting watch mode...');
const ctx = await esbuild.context(commonConfig);
await ctx.watch();
// Watch Tailwind CSS
execSync('npx tailwindcss -i ./static/css/app.css -o ./static/dist/app.css --watch');
console.log('Watching for changes...');
} else {
// Single build
build();
}
+6 -13
View File
@@ -87,7 +87,7 @@ templ AdminRoles(ctx context.Context, data RolesData) {
<button
data-role-id={ fmt.Sprint(role.ID) }
data-role-name={ role.Name }
onclick={ showRoleDialog(fmt.Sprintf("delete-role-dialog-%d", role.ID)) }
onclick={ showModal(fmt.Sprintf("delete-role-dialog-%d", role.ID)) }
class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">
<i class="fas fa-trash"></i>
</button>
@@ -269,7 +269,10 @@ templ AdminRoles(ctx context.Context, data RolesData) {
// RoleDialog for confirmation actions
templ RoleDialog(id string, title string, message string, confirmClass string, confirmText string, action string, roleID uint, roleName string) {
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<!-- Backdrop -->
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
<!-- Modal content -->
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<div class="p-6 text-center">
@@ -288,7 +291,7 @@ templ RoleDialog(id string, title string, message string, confirmClass string, c
onclick={ triggerRoleDelete(id, roleID, roleName) }>
{ confirmText }
</button>
<button type="button" data-modal-hide={ id } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
<button type="button" onclick={ closeModal(id) } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
Cancel
</button>
</div>
@@ -297,16 +300,6 @@ templ RoleDialog(id string, title string, message string, confirmClass string, c
</div>
}
script hideRoleDialog(id string) {
document.getElementById(id).classList.add("hidden");
document.getElementById(id).classList.remove("flex");
}
script showRoleDialog(id string) {
document.getElementById(id).classList.remove("hidden");
document.getElementById(id).classList.add("flex");
}
script triggerRoleDelete(dialogId string, roleID uint, roleName string) {
// Hide the dialog
document.getElementById(dialogId).classList.add("hidden");
+24 -9
View File
@@ -8,7 +8,10 @@ import (
// Dialog component for confirmation dialogs using Flowbite modal
templ ConfigDialog(id string, title string, message string, confirmClass string, confirmText string, action string, configID uint, configName string) {
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<!-- Backdrop -->
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
<!-- Modal content -->
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<div class="p-6 text-center">
@@ -30,7 +33,7 @@ templ ConfigDialog(id string, title string, message string, confirmClass string,
onclick={ triggerConfigDelete(id, configID, configName) }>
{ confirmText }
</button>
<button type="button" data-modal-hide={ id } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
<button type="button" onclick={ closeModal(id) } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
Cancel
</button>
</div>
@@ -39,14 +42,26 @@ templ ConfigDialog(id string, title string, message string, confirmClass string,
</div>
}
script hideConfigDialog(id string) {
document.getElementById(id).classList.add("hidden");
document.getElementById(id).classList.remove("flex");
script closeModal(id string) {
const modal = document.getElementById(id);
const backdrop = document.getElementById(id + '-backdrop');
if (modal) {
modal.classList.add('hidden');
modal.classList.remove('flex');
}
if (backdrop) {
backdrop.remove();
}
document.body.style.overflow = '';
}
script showConfigDialog(id string) {
document.getElementById(id).classList.remove("hidden");
document.getElementById(id).classList.add("flex");
script showModal(id string) {
const modal = document.getElementById(id);
if (modal) {
modal.classList.remove('hidden');
modal.classList.add('flex');
document.body.style.overflow = 'hidden';
}
}
script triggerConfigDelete(dialogId string, configID uint, configName string) {
@@ -429,7 +444,7 @@ templ Configs(ctx context.Context, data ConfigsData) {
)
<button
type="button"
onclick={ showConfigDialog(fmt.Sprintf("delete-config-dialog-%d", config.ID)) }
onclick={ showModal(fmt.Sprintf("delete-config-dialog-%d", config.ID)) }
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800">
<i class="fas fa-trash-alt w-3.5 h-3.5 mr-1.5"></i>
Delete
+10 -21
View File
@@ -76,7 +76,10 @@ func formatFileSize(size int64) string {
// Updated Dialog component using Flowbite
templ FileMetadataDialog(id string, title string, message string, confirmClass string, confirmText string, action string, fileID uint, fileName string, section string) {
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<!-- Backdrop -->
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
<!-- Modal content -->
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<div class="p-6 text-center">
@@ -112,7 +115,7 @@ templ FileMetadataDialog(id string, title string, message string, confirmClass s
{ confirmText }
</button>
}
<button type="button" data-modal-hide={ id } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
<button type="button" onclick={ closeModal(id) } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
Cancel
</button>
</div>
@@ -121,16 +124,6 @@ templ FileMetadataDialog(id string, title string, message string, confirmClass s
</div>
}
script hideFileDialog(id string) {
document.getElementById(id).classList.add("hidden");
document.getElementById(id).classList.remove("flex");
}
script showFileDialog(id string) {
document.getElementById(id).classList.remove("hidden");
document.getElementById(id).classList.add("flex");
}
script triggerFileDelete(dialogId string, fileID uint, fileName string) {
// Hide the dialog
document.getElementById(dialogId).classList.add("hidden");
@@ -211,7 +204,7 @@ templ FileMetadataListPartial(data FileMetadataListData) {
</a>
<button
type="button"
onclick={ showFileDialog(fmt.Sprintf("delete-file-dialog-%d", file.ID)) }
onclick={ showModal(fmt.Sprintf("delete-file-dialog-%d", file.ID)) }
class="font-medium text-red-600 dark:text-red-500 hover:underline">
<i class="fas fa-trash"></i>
</button>
@@ -392,9 +385,7 @@ templ FileMetadataList(ctx context.Context, data FileMetadataListData) {
});
// Track all HTMX events for debugging
document.addEventListener('htmx:beforeRequest', function(event) {
console.log("HTMX before request:", event.detail);
document.addEventListener('htmx:beforeRequest', function(event) {
// Check if this is a DELETE request by examining the URL and method
const path = event.detail.path;
const method = event.detail.verb;
@@ -411,9 +402,7 @@ templ FileMetadataList(ctx context.Context, data FileMetadataListData) {
});
// Track HTMX after-request events for file deletion
document.addEventListener('htmx:afterRequest', function(event) {
console.log("HTMX after request:", event.detail);
document.addEventListener('htmx:afterRequest', function(event) {
// Check for file deletion multiple ways
const isDeleteRequest =
// Check global flag from the triggerFileDelete function
@@ -970,7 +959,7 @@ templ FileMetadataDetails(ctx context.Context, data FileMetadataDetailsData) {
</a>
<button
type="button"
onclick={ showFileDialog(fmt.Sprintf("delete-file-dialog-%d", data.File.ID)) }
onclick={ showModal(fmt.Sprintf("delete-file-dialog-%d", data.File.ID)) }
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-sm px-5 py-2.5 dark:bg-red-600 dark:hover:bg-red-700 focus:outline-none dark:focus:ring-red-800">
<i class="fas fa-trash mr-2"></i> Delete Record
</button>
@@ -1336,7 +1325,7 @@ templ FileMetadataSearchContent(data FileMetadataSearchData) {
</a>
<button
type="button"
onclick={ showFileDialog(fmt.Sprintf("delete-file-dialog-%d", file.ID)) }
onclick={ showModal(fmt.Sprintf("delete-file-dialog-%d", file.ID)) }
class="font-medium text-red-600 dark:text-red-500 hover:underline">
<i class="fas fa-trash"></i>
</button>
+8 -19
View File
@@ -8,7 +8,10 @@ import (
// Dialog component for confirmation dialogs using Flowbite modal
templ JobDialog(id string, title string, message string, confirmClass string, confirmText string, action string, jobID uint, jobName string) {
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<!-- Backdrop -->
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
<!-- Modal content -->
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<div class="p-6 text-center">
@@ -30,7 +33,7 @@ templ JobDialog(id string, title string, message string, confirmClass string, co
onclick={ triggerJobDelete(id, jobID, jobName) }>
{ confirmText }
</button>
<button type="button" data-modal-hide={ id } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
<button type="button" onclick={ closeModal(id) } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
Cancel
</button>
</div>
@@ -39,16 +42,6 @@ templ JobDialog(id string, title string, message string, confirmClass string, co
</div>
}
script hideJobDialog(id string) {
document.getElementById(id).classList.add("hidden");
document.getElementById(id).classList.remove("flex");
}
script showJobDialog(id string) {
document.getElementById(id).classList.remove("hidden");
document.getElementById(id).classList.add("flex");
}
script triggerJobDelete(dialogId string, jobID uint, jobName string) {
// Hide the dialog
document.getElementById(dialogId).classList.add("hidden");
@@ -196,9 +189,7 @@ templ Jobs(ctx context.Context, data JobsData) {
});
// Track all HTMX events for debugging
document.addEventListener('htmx:beforeRequest', function(event) {
console.log("HTMX before request:", event.detail);
document.addEventListener('htmx:beforeRequest', function(event) {
// Check if this is a DELETE request by examining the URL and method
const path = event.detail.path;
const method = event.detail.verb;
@@ -215,9 +206,7 @@ templ Jobs(ctx context.Context, data JobsData) {
});
// Track HTMX after-request events for job deletion
document.addEventListener('htmx:afterRequest', function(event) {
console.log("HTMX after request:", event.detail);
document.addEventListener('htmx:afterRequest', function(event) {
// Check for job deletion multiple ways
const isDeleteRequest =
// Check global flag from the triggerJobDelete function
@@ -395,7 +384,7 @@ templ Jobs(ctx context.Context, data JobsData) {
)
<button
type="button"
onclick={ showJobDialog(fmt.Sprintf("delete-job-dialog-%d", job.ID)) }
onclick={ showModal(fmt.Sprintf("delete-job-dialog-%d", job.ID)) }
class="focus:outline-none text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:ring-red-300 font-medium rounded-lg text-xs px-3 py-1.5 dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-900">
<i class="fas fa-trash mr-1"></i>
Delete
+116 -96
View File
@@ -54,42 +54,36 @@ templ LayoutWithContext(title string, ctx context.Context) {
<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"/>
<!-- 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>
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',
// 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';
}
},
fontFamily: {
sans: ['Inter var', 'ui-sans-serif', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'Helvetica Neue', 'Arial', 'sans-serif'],
}
}
});
});
}
}
})();
</script>
<style>
html, body {
@@ -146,32 +140,6 @@ templ LayoutWithContext(title string, ctx context.Context) {
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) {
@@ -269,20 +237,100 @@ templ LayoutWithContext(title string, ctx context.Context) {
<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>
<!-- 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">
<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 -->
@@ -447,40 +495,12 @@ templ LayoutWithContext(title string, ctx context.Context) {
</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-settings"]');
const adminDropdown = document.getElementById('dropdown-settings');
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>
<!-- Scripts -->
<!-- Alpine.js and dependencies -->
<script defer src="/static/dist/vendor.js"></script>
<!-- Application scripts -->
<script defer src="/static/dist/app.js"></script>
<script defer src="/static/dist/init.js"></script>
</body>
</html>
}
+8 -19
View File
@@ -7,7 +7,10 @@ import (
// Dialog component for confirmation dialogs using Flowbite modal
templ NotificationDialog(id string, title string, message string, confirmClass string, confirmText string, action string, serviceID uint, serviceName string) {
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<!-- Backdrop -->
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
<!-- Modal content -->
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<div class="p-6 text-center">
@@ -28,7 +31,7 @@ templ NotificationDialog(id string, title string, message string, confirmClass s
onclick={ triggerServiceDelete(id, serviceID, serviceName) }>
{ confirmText }
</button>
<button type="button" data-modal-hide={ id } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
<button type="button" onclick={ closeModal(id) } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
Cancel
</button>
</div>
@@ -37,16 +40,6 @@ templ NotificationDialog(id string, title string, message string, confirmClass s
</div>
}
script hideNotificationDialog(id string) {
document.getElementById(id).classList.add("hidden");
document.getElementById(id).classList.remove("flex");
}
script showNotificationDialog(id string) {
document.getElementById(id).classList.remove("hidden");
document.getElementById(id).classList.add("flex");
}
script triggerServiceDelete(dialogId string, serviceID uint, serviceName string) {
// Hide the dialog
document.getElementById(dialogId).classList.add("hidden");
@@ -143,9 +136,7 @@ templ Notifications(ctx context.Context, data SettingsNotificationsData) {
}
// Track all HTMX events for debugging
document.addEventListener('htmx:beforeRequest', function(event) {
console.log("HTMX before request:", event.detail);
document.addEventListener('htmx:beforeRequest', function(event) {
// Check if this is a DELETE request for a notification service
const path = event.detail.path;
const method = event.detail.verb;
@@ -161,9 +152,7 @@ templ Notifications(ctx context.Context, data SettingsNotificationsData) {
}
});
document.addEventListener('htmx:afterRequest', function(event) {
console.log("HTMX after request:", event.detail);
document.addEventListener('htmx:afterRequest', function(event) {
// Check for notification service deletion multiple ways
const isDeleteRequest =
// Check global flag from the triggerServiceDelete function
@@ -369,7 +358,7 @@ templ Notifications(ctx context.Context, data SettingsNotificationsData) {
)
<button
type="button"
onclick={ showNotificationDialog(fmt.Sprintf("delete-notification-dialog-%d", service.ID)) }
onclick={ showModal(fmt.Sprintf("delete-notification-dialog-%d", service.ID)) }
class="text-red-500 bg-white focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-200 rounded-lg text-sm p-2 dark:bg-gray-800 dark:text-red-400 dark:hover:bg-gray-700 dark:hover:text-white dark:focus:ring-gray-700"
>
<i class="fas fa-trash-alt"></i>
+4 -4
View File
@@ -8,7 +8,7 @@ templ FTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_host" name="dest_host" x-model="destinationHost" x-bind:required="requiresDestination"
<input type="text" id="dest_host" name="dest_host" x-model="destHost" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="ftp.example.com" />
</div>
@@ -21,7 +21,7 @@ templ FTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-network-wired text-gray-400 dark:text-gray-500"></i>
</div>
<input type="number" id="dest_port" name="dest_port" x-model="destinationPort"
<input type="number" id="dest_port" name="dest_port" x-model="destPort"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="21" min="1" max="65535" />
</div>
@@ -34,7 +34,7 @@ templ FTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="username" />
</div>
@@ -47,7 +47,7 @@ templ FTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" x-bind:required="requiresDestination"
<input type="password" id="dest_password" name="dest_password" x-model="destPassword" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="Enter password" />
</div>
@@ -23,7 +23,7 @@ templ GoogleDriveDestinationForm() {
</p>
</div>
<div x-show="!useBuiltinAuthDestination">
<div x-show="!useBuiltinAuthDest">
<div class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 dark:bg-yellow-900/30 dark:text-yellow-300" role="alert">
<div class="flex">
<i class="fas fa-exclamation-triangle mr-2 flex-shrink-0"></i>
@@ -37,9 +37,9 @@ templ GoogleDriveDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-id-card text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destinationClientId"
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destClientId"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="Your Google API Client ID" x-bind:required="!useBuiltinAuthDestination" />
placeholder="Your Google API Client ID" x-bind:required="!useBuiltinAuthDest" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">OAuth 2.0 Client ID from Google Cloud Console</p>
</div>
@@ -50,9 +50,9 @@ templ GoogleDriveDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destinationClientSecret"
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destClientSecret"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="Your Google API Client Secret" x-bind:required="!useBuiltinAuthDestination" />
placeholder="Your Google API Client Secret" x-bind:required="!useBuiltinAuthDest" />
</div>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">OAuth 2.0 Client Secret from Google Cloud Console</p>
</div>
@@ -79,7 +79,7 @@ templ GoogleDriveDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-fingerprint text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_drive_id" name="dest_drive_id" x-model="destinationDriveId"
<input type="text" id="dest_drive_id" name="dest_drive_id" x-model="destDriveId"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="Specific Google Drive ID" />
</div>
@@ -94,7 +94,7 @@ templ GoogleDriveDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-users text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_team_drive" name="dest_team_drive" x-model="destinationTeamDrive"
<input type="text" id="dest_team_drive" name="dest_team_drive" x-model="destTeamDrive"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="Team Drive ID" />
</div>
@@ -1,10 +1,10 @@
package destination
templ GooglePhotosDestinationForm() {
<div class="space-y-6 mt-4" x-init="$watch('useBuiltinAuthDestination', value => {
<div class="space-y-6 mt-4" x-init="$watch('useBuiltinAuthDest', value => {
if(value) {
destinationClientId = '';
destinationClientSecret = '';
destClientId = '';
destClientSecret = '';
}
})">
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 dark:bg-blue-900/30 dark:text-blue-300" role="alert">
@@ -38,7 +38,7 @@ templ GooglePhotosDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-id-card text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destinationClientId"
<input type="text" id="dest_client_id" name="dest_client_id" x-model="destClientId"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
x-bind:disabled="useBuiltinAuthDest"
placeholder="Google Photos OAuth Client ID" />
@@ -57,7 +57,7 @@ templ GooglePhotosDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destinationClientSecret"
<input type="password" id="dest_client_secret" name="dest_client_secret" x-model="destClientSecret"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
x-bind:disabled="useBuiltinAuthDest"
placeholder="Google Photos OAuth Client Secret" />
+4 -4
View File
@@ -15,7 +15,7 @@ templ MinIODestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destinationEndpoint" x-bind:required="requiresDestination"
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destEndpoint" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="https://minio.example.com" />
</div>
@@ -30,7 +30,7 @@ templ MinIODestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-archive text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destinationBucket" x-bind:required="requiresDestination"
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destBucket" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="my-bucket" />
</div>
@@ -45,7 +45,7 @@ templ MinIODestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_access_key" name="dest_access_key" x-model="destinationAccessKey" x-bind:required="requiresDestination"
<input type="text" id="dest_access_key" name="dest_access_key" x-model="destAccessKey" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="minioadmin" />
</div>
@@ -60,7 +60,7 @@ templ MinIODestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_secret_key" name="dest_secret_key" x-model="destinationSecretKey" x-bind:required="requiresDestination"
<input type="password" id="dest_secret_key" name="dest_secret_key" x-model="destSecretKey" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="minioadmin" />
</div>
@@ -15,7 +15,7 @@ templ NextCloudDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-cloud text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destinationEndpoint" x-bind:required="requiresDestination"
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destEndpoint" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="https://nextcloud.example.com" />
</div>
@@ -30,7 +30,7 @@ templ NextCloudDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="nextcloud_username" />
</div>
@@ -45,7 +45,7 @@ templ NextCloudDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" x-bind:required="requiresDestination"
<input type="password" id="dest_password" name="dest_password" x-model="destPassword" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="NextCloud password" />
</div>
+4 -4
View File
@@ -15,7 +15,7 @@ templ S3DestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-globe text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_region" name="dest_region" x-model="destinationRegion" x-bind:required="requiresDestination"
<input type="text" id="dest_region" name="dest_region" x-model="destRegion" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="us-east-1" />
</div>
@@ -30,7 +30,7 @@ templ S3DestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-archive text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destinationBucket" x-bind:required="requiresDestination"
<input type="text" id="dest_bucket" name="dest_bucket" x-model="destBucket" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="my-bucket" />
</div>
@@ -45,7 +45,7 @@ templ S3DestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-key text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_access_key" name="dest_access_key" x-model="destinationAccessKey" x-bind:required="requiresDestination"
<input type="text" id="dest_access_key" name="dest_access_key" x-model="destAccessKey" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="AKIAIOSFODNN7EXAMPLE" />
</div>
@@ -60,7 +60,7 @@ templ S3DestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_secret_key" name="dest_secret_key" x-model="destinationSecretKey" x-bind:required="requiresDestination"
<input type="password" id="dest_secret_key" name="dest_secret_key" x-model="destSecretKey" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" />
</div>
+4 -4
View File
@@ -8,7 +8,7 @@ templ SFTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_host" name="dest_host" x-model="destinationHost" x-bind:required="requiresDestination"
<input type="text" id="dest_host" name="dest_host" x-model="destHost" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="sftp.example.com" />
</div>
@@ -21,7 +21,7 @@ templ SFTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-network-wired text-gray-400 dark:text-gray-500"></i>
</div>
<input type="number" id="dest_port" name="dest_port" x-model="destinationPort"
<input type="number" id="dest_port" name="dest_port" x-model="destPort"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="22" min="1" max="65535" />
</div>
@@ -34,7 +34,7 @@ templ SFTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="username" />
</div>
@@ -61,7 +61,7 @@ templ SFTPDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword"
<input type="password" id="dest_password" name="dest_password" x-model="destPassword"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="Enter password" x-bind:required="destAuthType === 'password'" />
</div>
+5 -5
View File
@@ -15,7 +15,7 @@ templ SMBDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-server text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_host" name="dest_host" x-model="destinationHost" x-bind:required="requiresDestination"
<input type="text" id="dest_host" name="dest_host" x-model="destHost" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="192.168.1.100 or server.example.com" />
</div>
@@ -30,7 +30,7 @@ templ SMBDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-share-alt text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_share" name="dest_share" x-model="destinationShare" x-bind:required="requiresDestination"
<input type="text" id="dest_share" name="dest_share" x-model="destShare" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="Documents" />
</div>
@@ -45,7 +45,7 @@ templ SMBDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-sitemap text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_domain" name="dest_domain" x-model="destinationDomain"
<input type="text" id="dest_domain" name="dest_domain" x-model="destDomain"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="WORKGROUP or DOMAIN" />
</div>
@@ -60,7 +60,7 @@ templ SMBDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="smb_username" />
</div>
@@ -75,7 +75,7 @@ templ SMBDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" x-bind:required="requiresDestination"
<input type="password" id="dest_password" name="dest_password" x-model="destPassword" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="SMB password" />
</div>
@@ -15,7 +15,7 @@ templ WebDAVDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-globe text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destinationEndpoint" x-bind:required="requiresDestination"
<input type="text" id="dest_endpoint" name="dest_endpoint" x-model="destEndpoint" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="https://webdav.example.com" />
</div>
@@ -30,7 +30,7 @@ templ WebDAVDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-user text-gray-400 dark:text-gray-500"></i>
</div>
<input type="text" id="dest_user" name="dest_user" x-model="destinationUser" x-bind:required="requiresDestination"
<input type="text" id="dest_user" name="dest_user" x-model="destUser" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="webdav_username" />
</div>
@@ -45,7 +45,7 @@ templ WebDAVDestinationForm() {
<div class="absolute inset-y-0 start-0 flex items-center ps-3.5 pointer-events-none">
<i class="fas fa-lock text-gray-400 dark:text-gray-500"></i>
</div>
<input type="password" id="dest_password" name="dest_password" x-model="destinationPassword" x-bind:required="requiresDestination"
<input type="password" id="dest_password" name="dest_password" x-model="destPassword" x-bind:required="requiresDestination"
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 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"
placeholder="WebDAV password" />
</div>
+7 -16
View File
@@ -117,9 +117,7 @@ templ UserManagementContent(data UserManagementData) {
});
// Track user operations in HTMX events
document.addEventListener('htmx:beforeRequest', function(event) {
console.log("HTMX before request:", event.detail);
document.addEventListener('htmx:beforeRequest', function(event) {
// Check if this is a DELETE request for a user
const path = event.detail.path;
const method = event.detail.verb;
@@ -421,7 +419,7 @@ templ userList(users []db.User) {
class="text-white bg-red-700 hover:bg-red-800 focus:ring-4 focus:outline-none focus:ring-red-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-red-600 dark:hover:bg-red-700 dark:focus:ring-red-800"
data-user-email={ user.Email }
data-user-id={ strconv.Itoa(int(user.ID)) }
onclick={ showUserDialog(fmt.Sprintf("delete-user-dialog-%d", user.ID)) }
onclick={ showModal(fmt.Sprintf("delete-user-dialog-%d", user.ID)) }
>
<i class="fas fa-trash-alt w-3.5 h-3.5 mr-1.5"></i> Delete
</button>
@@ -634,7 +632,10 @@ func hasRole(user *db.User, roleID uint) bool {
// UserDialog for confirmation actions
templ UserDialog(id string, title string, message string, confirmClass string, confirmText string, action string, userID uint, userEmail string) {
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm">
<div id={ id } tabindex="-1" aria-hidden="true" class="hidden fixed top-0 right-0 left-0 z-50 justify-center items-center w-full md:inset-0 h-[calc(100%-1rem)] max-h-full">
<!-- Backdrop -->
<div id={ fmt.Sprintf("%s-backdrop", id) } class="fixed inset-0 bg-gray-900/50 dark:bg-gray-900/80 backdrop-blur-sm"></div>
<!-- Modal content -->
<div class="relative p-4 w-full max-w-md max-h-full mx-auto">
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<div class="p-6 text-center">
@@ -654,7 +655,7 @@ templ UserDialog(id string, title string, message string, confirmClass string, c
onclick={ triggerUserDelete(id, userID, userEmail) }>
{ confirmText }
</button>
<button type="button" data-modal-hide={ id } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
<button type="button" onclick={ closeModal(id) } class="text-gray-500 bg-white hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-200 rounded-lg border border-gray-200 text-sm font-medium px-5 py-2.5 hover:text-gray-900 focus:z-10 dark:bg-gray-700 dark:text-gray-300 dark:border-gray-500 dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-gray-600">
Cancel
</button>
</div>
@@ -663,16 +664,6 @@ templ UserDialog(id string, title string, message string, confirmClass string, c
</div>
}
script hideUserDialog(id string) {
document.getElementById(id).classList.add("hidden");
document.getElementById(id).classList.remove("flex");
}
script showUserDialog(id string) {
document.getElementById(id).classList.remove("hidden");
document.getElementById(id).classList.add("flex");
}
script triggerUserDelete(dialogId string, userID uint, userEmail string) {
// Hide the dialog
document.getElementById(dialogId).classList.add("hidden");
+7 -4
View File
@@ -653,7 +653,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
case "webdav":
args := []string{
"config", "create", sourceName, "webdav",
"url", config.SourceHost,
"url", config.SourceEndpoint,
"user", config.SourceUser,
"pass", config.SourcePassword,
"--non-interactive",
@@ -668,7 +668,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
case "nextcloud":
args := []string{
"config", "create", sourceName, "webdav",
"url", config.SourceHost,
"url", config.SourceEndpoint,
"user", config.SourceUser,
"pass", config.SourcePassword,
"vendor", "nextcloud",
@@ -760,6 +760,9 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
}
destName := fmt.Sprintf("dest_%d", config.ID)
fmt.Println("config", config)
switch config.DestinationType {
case "sftp":
args := []string{
@@ -875,7 +878,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
case "webdav":
args := []string{
"config", "create", destName, "webdav",
"url", config.DestHost,
"url", config.DestEndpoint,
"user", config.DestUser,
"pass", config.DestPassword,
"--non-interactive",
@@ -890,7 +893,7 @@ func (db *DB) GenerateRcloneConfig(config *TransferConfig) error {
case "nextcloud":
args := []string{
"config", "create", destName, "webdav",
"url", config.DestHost,
"url", config.DestEndpoint,
"user", config.DestUser,
"pass", config.DestPassword,
"vendor", "nextcloud",
+49 -2
View File
@@ -8,6 +8,7 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
@@ -23,6 +24,40 @@ import (
//go:embed static
var staticFiles embed.FS
// setCacheHeaders sets appropriate cache headers based on file type
func setCacheHeaders(c *gin.Context, path string) {
// Set cache headers based on file type
if strings.HasSuffix(path, ".js") || strings.HasSuffix(path, ".css") {
c.Header("Cache-Control", "public, max-age=31536000") // Cache for 1 year
} else if strings.HasSuffix(path, ".png") || strings.HasSuffix(path, ".jpg") || strings.HasSuffix(path, ".webp") {
c.Header("Cache-Control", "public, max-age=86400") // Cache for 1 day
} else {
c.Header("Cache-Control", "no-cache")
}
}
// setContentType sets the correct content type based on file extension
func setContentType(c *gin.Context, path string) {
switch {
case strings.HasSuffix(path, ".js"):
c.Header("Content-Type", "application/javascript")
case strings.HasSuffix(path, ".css"):
c.Header("Content-Type", "text/css")
case strings.HasSuffix(path, ".png"):
c.Header("Content-Type", "image/png")
case strings.HasSuffix(path, ".jpg"), strings.HasSuffix(path, ".jpeg"):
c.Header("Content-Type", "image/jpeg")
case strings.HasSuffix(path, ".webp"):
c.Header("Content-Type", "image/webp")
case strings.HasSuffix(path, ".svg"):
c.Header("Content-Type", "image/svg+xml")
case strings.HasSuffix(path, ".woff2"):
c.Header("Content-Type", "font/woff2")
case strings.HasSuffix(path, ".woff"):
c.Header("Content-Type", "font/woff")
}
}
func main() {
// Set Gin to release mode
gin.SetMode(gin.ReleaseMode)
@@ -110,12 +145,24 @@ func main() {
}))
router.Use(gin.Recovery())
// Serve embedded static files
// Serve embedded static files with proper content types and caching
staticFS, err := fs.Sub(staticFiles, "static")
if err != nil {
log.Fatalf("Failed to create sub-filesystem: %v", err)
}
router.StaticFS("/static", http.FS(staticFS))
// Custom static file handler
router.GET("/static/*filepath", func(c *gin.Context) {
path := c.Param("filepath")
// Set content type and cache headers
setContentType(c, path)
setCacheHeaders(c, path)
// Serve the file
c.FileFromFS(path, http.FS(staticFS))
})
log.Printf("Embedded static files configured for serving")
// Initialize web handlers
+1872
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
{
"name": "gomft",
"version": "1.0.0",
"description": "GoMFT static assets bundling",
"type": "module",
"scripts": {
"build": "node build.js",
"watch": "node build.js --watch",
"postinstall": "npm run build"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.4.0",
"alpinejs": "^3.13.5",
"esbuild": "^0.20.1",
"flowbite": "^2.2.1",
"htmx.org": "^1.9.10",
"tailwindcss": "^3.4.1"
}
}
+51
View File
@@ -1,3 +1,54 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Custom styles */
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;
}
}
/* Dark mode styles */
body.dark {
background-color: #111827; /* gray-900 */
}
body.dark .configs-page,
body.dark .jobs-page {
background-color: #111827 !important; /* gray-900 */
min-height: 100vh;
}
#jobs-container.dark, #configs-container.dark {
background-color: #111827 !important;
min-height: 100vh;
}
body.dark .bg-white {
background-color: #1f2937 !important;
}
/* Base styles */
:root {
--primary-color: #2563eb;
+32
View File
@@ -0,0 +1,32 @@
// 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-settings"]');
const adminDropdown = document.getElementById('dropdown-settings');
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');
});
}
});
+53
View File
@@ -0,0 +1,53 @@
// Import HTMX
import 'htmx.org';
// Import Alpine.js
import Alpine from 'alpinejs';
window.Alpine = Alpine;
// Import Flowbite
import 'flowbite';
import 'flowbite/dist/flowbite.css';
// Initialize Flowbite components
document.addEventListener('DOMContentLoaded', () => {
// Initialize Alpine.js
Alpine.start();
// Initialize Flowbite modals
const modalTriggers = document.querySelectorAll('[data-modal-target]');
modalTriggers.forEach(trigger => {
const targetId = trigger.getAttribute('data-modal-target');
const targetModal = document.getElementById(targetId);
if (targetModal) {
// Show modal
trigger.addEventListener('click', () => {
targetModal.classList.remove('hidden');
targetModal.classList.add('flex');
// Add backdrop
document.body.style.overflow = 'hidden';
});
// Handle modal hide buttons
const hideButtons = targetModal.querySelectorAll('[data-modal-hide]');
hideButtons.forEach(button => {
button.addEventListener('click', () => {
targetModal.classList.add('hidden');
targetModal.classList.remove('flex');
// Remove backdrop
document.body.style.overflow = '';
});
});
// Handle clicking outside the modal
targetModal.addEventListener('click', (event) => {
if (event.target === targetModal || event.target.classList.contains('fixed')) {
targetModal.classList.add('hidden');
targetModal.classList.remove('flex');
// Remove backdrop
document.body.style.overflow = '';
}
});
}
});
});
+33
View File
@@ -0,0 +1,33 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./components/**/*.{html,js,templ,go}",
"./node_modules/flowbite/**/*.js"
],
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'],
}
}
},
plugins: [
require('flowbite/plugin')
],
}