From 30d07253311abc854ce7aad5aa15a4de51f42e7d Mon Sep 17 00:00:00 2001 From: StarFleetCPTN Date: Sun, 23 Mar 2025 11:42:16 -0700 Subject: [PATCH] feat: Enhance user management and dashboard components - Updated user management templates to improve user experience with a new layout and success/error message handling. - Implemented password hashing for user creation and management, enhancing security. - Refactored dashboard components to include better data presentation and responsiveness. - Added dark mode support for various components, ensuring a consistent user interface across themes. - Improved toast notification animations for better user feedback during actions. --- components/admin_database.templ | 16 +- components/admin_roles.templ | 56 +- components/dashboard.templ | 424 ++++++------ components/file_metadata.templ | 616 +++++++++-------- components/history.templ | 178 ++--- components/notifications.templ | 6 +- components/settings.templ | 28 +- components/user_management.templ | 146 ++-- internal/db/db.go | 26 + internal/web/handlers/admin_handlers.go | 704 ++++++++++++++++++++ internal/web/handlers/dashboard_handlers.go | 3 +- internal/web/handlers/routes.go | 6 +- internal/web/handlers/user_handlers.go | 208 +++--- 13 files changed, 1656 insertions(+), 761 deletions(-) diff --git a/components/admin_database.templ b/components/admin_database.templ index 85ec15f..20738bb 100644 --- a/components/admin_database.templ +++ b/components/admin_database.templ @@ -262,7 +262,7 @@ templ AdminDatabaseTools(ctx context.Context, backups []BackupFile) { // Create toast element const toast = document.createElement('div'); toast.id = 'toast-' + type + '-' + Date.now(); - toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800'; + toast.className = 'flex items-center w-full max-w-xs p-4 mb-4 rounded-lg shadow text-gray-500 bg-white dark:text-gray-400 dark:bg-gray-800 transform translate-y-16 opacity-0 transition-all duration-300 ease-out'; toast.role = 'alert'; // Set toast content based on type @@ -301,15 +301,25 @@ templ AdminDatabaseTools(ctx context.Context, backups []BackupFile) { // Add toast to container toastContainer.appendChild(toast); + // Trigger animation after a small delay to ensure the DOM has updated + setTimeout(() => { + toast.classList.remove('translate-y-16', 'opacity-0'); + toast.classList.add('translate-y-0', 'opacity-100'); + }, 10); + // Add event listener to close button const closeButton = toast.querySelector('button[data-dismiss-target]'); closeButton.addEventListener('click', function() { - toast.remove(); + // Animate out before removing + toast.classList.add('opacity-0', 'translate-y-4'); + setTimeout(() => { + toast.remove(); + }, 300); }); // Auto-remove toast after 5 seconds setTimeout(() => { - toast.classList.add('opacity-0', 'transition-opacity', 'duration-300'); + toast.classList.add('opacity-0', 'translate-y-4'); setTimeout(() => { toast.remove(); }, 300); diff --git a/components/admin_roles.templ b/components/admin_roles.templ index a06d608..cddd4dc 100644 --- a/components/admin_roles.templ +++ b/components/admin_roles.templ @@ -94,14 +94,20 @@ templ AdminRoles(ctx context.Context, data RolesData) {

Are you sure you want to delete this role?

-
- - -
+
@@ -109,14 +115,52 @@ templ AdminRoles(ctx context.Context, data RolesData) { } diff --git a/components/dashboard.templ b/components/dashboard.templ index 2d00182..db5ca3c 100644 --- a/components/dashboard.templ +++ b/components/dashboard.templ @@ -43,214 +43,216 @@ func GetRcloneVersion() string { templ Dashboard(ctx context.Context, data DashboardData) { @LayoutWithContext("Dashboard", ctx) { -
- -
-

- - Dashboard -

-
- - -
-
- - -
- -
-
-

Active Transfers

-
- -
-
-
-
- { strconv.Itoa(data.ActiveTransfers) } -
-
- Currently in progress -
+
+
+ +
+

+ + Dashboard +

+
+ +
- -
-
-

Completed Today

-
- -
-
-
-
- { strconv.Itoa(data.CompletedToday) } -
-
- Successfully completed -
-
-
- - -
-
-

Failed Transfers

-
- -
-
-
-
- { strconv.Itoa(data.FailedTransfers) } -
-
- Require attention -
-
-
-
- - -
- -
-
-

- - Recent Jobs -

-
-
- if len(data.RecentJobs) == 0 { - -
-
- -
-

No recent jobs found

-

Get started by creating your first job.

- - - Create your first job - + +
+ +
+
+

Active Transfers

+
+
- } else { -
-
    - for _, job := range data.RecentJobs { -
  • -
    -
    - if job.Status == "completed" { - - - - } else if job.Status == "failed" { - - - - } else { - - - - } -
    -
    -

    - { getConfigNameForHistory(job, data.Configs) } -

    -
    - -

    - Started: { job.StartTime.Format("Jan 02, 2006 15:04:05") } -

    +
    +
    +
    + { strconv.Itoa(data.ActiveTransfers) } +
    +
    + Currently in progress +
    +
    +
    + + +
    +
    +

    Completed Today

    +
    + +
    +
    +
    +
    + { strconv.Itoa(data.CompletedToday) } +
    +
    + Successfully completed +
    +
    +
    + + +
    +
    +

    Failed Transfers

    +
    + +
    +
    +
    +
    + { strconv.Itoa(data.FailedTransfers) } +
    +
    + Require attention +
    +
    +
    +
    + + +
    + +
    +
    +

    + + Recent Jobs +

    +
    +
    + if len(data.RecentJobs) == 0 { + +
    +
    + +
    +

    No recent jobs found

    +

    Get started by creating your first job.

    + + + Create your first job + +
    + } else { +
    +
      + for _, job := range data.RecentJobs { +
    • +
      +
      + if job.Status == "completed" { + + + + } else if job.Status == "failed" { + + + + } else { + + + + }
      +
      +

      + { getConfigNameForHistory(job, data.Configs) } +

      +
      + +

      + Started: { job.StartTime.Format("Jan 02, 2006 15:04:05") } +

      +
      +
      + + View + +
      - - View - - -
    -
  • - } -
-
- - } + + } + +
+ + } +
-
- - -
-
-

- - Quick Actions -

-
-
- - - Create New Config - - - - Create New Job - - - - View Transfer History - - - -
-
-

System Status

-
-
-
-
- Server - - - Online - -
-
- Scheduler - - - Running - -
-
- Database - - - Connected - -
-
- Rclone - - - if data.RcloneVersion != "" { - { data.RcloneVersion } - } else { - Unknown - } - + + +
+
+

+ + Quick Actions +

+
+
+ + + Create New Config + + + + Create New Job + + + + View Transfer History + + + +
+
+

System Status

+
+
+
+
+ Server + + + Online + +
+
+ Scheduler + + + Running + +
+
+ Database + + + Connected + +
+
+ Rclone + + + if data.RcloneVersion != "" { + { data.RcloneVersion } + } else { + Unknown + } + +
@@ -258,6 +260,26 @@ templ Dashboard(ctx context.Context, data DashboardData) {
+ +
} } \ No newline at end of file diff --git a/components/file_metadata.templ b/components/file_metadata.templ index 7dfac29..a9a9a37 100644 --- a/components/file_metadata.templ +++ b/components/file_metadata.templ @@ -341,17 +341,15 @@ templ FileMetadataListPartial(data FileMetadataListData) { // Update the main FileMetadataList template to use the partial template templ FileMetadataList(ctx context.Context, data FileMetadataListData) { @LayoutWithContext("File Metadata", ctx) { -
-
-
-

+
+
+
+

+ if data.Job != nil { Files for Job: { data.Job.Name } } else { -

- - File Metadata -

+ File Metadata }

@@ -425,6 +423,26 @@ templ FileMetadataList(ctx context.Context, data FileMetadataListData) {
+ +
} } @@ -432,213 +450,239 @@ templ FileMetadataList(ctx context.Context, data FileMetadataListData) { // FileMetadataDetails renders detailed information about a file templ FileMetadataDetails(ctx context.Context, data FileMetadataDetailsData) { @LayoutWithContext("File Details", ctx) { -
- - -
- -
-
- { data.File.FileName } -
-

- File ID: { strconv.FormatUint(uint64(data.File.ID), 10) } -

+
+
+
+

+ + File Details: { data.File.FileName } +

+ + Back to Files +
- -
-
- -
-
- File Information -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Filename - - { data.File.FileName } -
- Size - - { formatFileSize(data.File.FileSize) } -
- Hash - - if data.File.FileHash != "" { - { data.File.FileHash } - } else { - Not available - } -
- Status - - - { data.File.Status } - -
- Original Path - -
- - { data.File.OriginalPath } -
-
- Destination Path - -
- - { data.File.DestinationPath } -
-
-
-
- - -
-
- Processing Information -
-
- - - - - - - - - - - - - - - - - - - if data.File.Status == "error" && data.File.ErrorMessage != "" { +
+ +
+
+ { data.File.FileName } +
+

+ File ID: { strconv.FormatUint(uint64(data.File.ID), 10) } +

+
+ + +
+
+ +
+
+ File Information +
+
+
- Job - - - { data.File.Job.Name } - -
- Processed Time - -
- - { data.File.ProcessedTime.Format("2006-01-02 15:04:05") } -
-
- Creation Time - -
- - { data.File.CreationTime.Format("2006-01-02 15:04:05") } -
-
- Modification Time - -
- - { data.File.ModTime.Format("2006-01-02 15:04:05") } -
-
+ + + + + + + + + + + + + + + + + - } - - - - - - - - - -
- Error + Filename + + { data.File.FileName } +
+ Size + + { formatFileSize(data.File.FileSize) } +
+ Hash -
- - { data.File.ErrorMessage } + if data.File.FileHash != "" { + { data.File.FileHash } + } else { + Not available + } +
+ Status + + + { data.File.Status } + +
+ Original Path + +
+ + { data.File.OriginalPath }
- Record Created - - { data.File.CreatedAt.Format("2006-01-02 15:04:05") } -
- Record Updated - - { data.File.UpdatedAt.Format("2006-01-02 15:04:05") } -
+ + + Destination Path + + +
+ + { data.File.DestinationPath } +
+ + + + +
+
+ + +
+
+ Processing Information +
+
+ + + + + + + + + + + + + + + + + + + if data.File.Status == "error" && data.File.ErrorMessage != "" { + + + + + } + + + + + + + + + +
+ Job + + + { data.File.Job.Name } + +
+ Processed Time + +
+ + { data.File.ProcessedTime.Format("2006-01-02 15:04:05") } +
+
+ Creation Time + +
+ + { data.File.CreationTime.Format("2006-01-02 15:04:05") } +
+
+ Modification Time + +
+ + { data.File.ModTime.Format("2006-01-02 15:04:05") } +
+
+ Error + +
+ + { data.File.ErrorMessage } +
+
+ Record Created + + { data.File.CreatedAt.Format("2006-01-02 15:04:05") } +
+ Record Updated + + { data.File.UpdatedAt.Format("2006-01-02 15:04:05") } +
+
-
- - - @FileMetadataDialog( - fmt.Sprintf("delete-file-dialog-%d", data.File.ID), - "Delete File Metadata", - fmt.Sprintf("Are you sure you want to delete the metadata for '%s'? This cannot be undone.", data.File.FileName), - "btn-danger", - "Delete", - "delete", - data.File.ID, - data.File.FileName, - "details", - ) - - -
- - Back to Files - - + + + @FileMetadataDialog( + fmt.Sprintf("delete-file-dialog-%d", data.File.ID), + "Delete File Metadata", + fmt.Sprintf("Are you sure you want to delete the metadata for '%s'? This cannot be undone.", data.File.FileName), + "btn-danger", + "Delete", + "delete", + data.File.ID, + data.File.FileName, + "details", + ) + + +
+ + Back to Files + + +
+ +
} } @@ -646,82 +690,108 @@ templ FileMetadataDetails(ctx context.Context, data FileMetadataDetailsData) { // FileMetadataSearch renders an advanced search form for file metadata templ FileMetadataSearch(ctx context.Context, data FileMetadataSearchData) { @LayoutWithContext("Search Files", ctx) { -
- - -
-
Advanced File Search
-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
- +
+
+
+

+ + File Metadata Search +

+ + Back to Files + +
+ +
+
Advanced File Search
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+ +
+ + +
+
+
Search Results ({ strconv.FormatInt(data.TotalCount, 10) })
+ + Page { strconv.Itoa(data.Page) } of { strconv.Itoa(data.TotalPages) } + +
+
+ @FileMetadataSearchContent(data)
- -
- - -
-
-
Search Results ({ strconv.FormatInt(data.TotalCount, 10) })
- - Page { strconv.Itoa(data.Page) } of { strconv.Itoa(data.TotalPages) } - -
-
- @FileMetadataSearchContent(data)
+ +
} } diff --git a/components/history.templ b/components/history.templ index ed77e64..ba0ba9a 100644 --- a/components/history.templ +++ b/components/history.templ @@ -264,91 +264,113 @@ templ HistoryContent(ctx context.Context, data HistoryData) { templ History(ctx context.Context, data HistoryData) { @LayoutWithContext("Transfer History", ctx) { -
- -
-

- - Transfer History -

-
- - Total: { fmt.Sprint(data.Total) } transfers -
-
- - -
-
-
-
- -
- - - +
+
+ +
+

+ + Transfer History +

+
+ + Total: { fmt.Sprint(data.Total) } transfers
-
- - if data.SearchTerm != "" { +
+ + +
+ +
+
+ +
+ + + +
+
- } -
-
- -
- -
- -
- @HistoryContent(ctx, data) + if data.SearchTerm != "" { + + } +
+
+ +
+ +
+ +
+ @HistoryContent(ctx, data) +
+ +
} } diff --git a/components/notifications.templ b/components/notifications.templ index 0c1364c..741069c 100644 --- a/components/notifications.templ +++ b/components/notifications.templ @@ -139,9 +139,9 @@ templ NotificationDropdown(data NotificationsData) {
-
- -
+ //
+ // + //
diff --git a/components/settings.templ b/components/settings.templ index 18c599b..5b2d119 100644 --- a/components/settings.templ +++ b/components/settings.templ @@ -28,9 +28,10 @@ type SettingsData struct { templ Settings(ctx context.Context, data SettingsData) { @LayoutWithContext("Application Settings", ctx) { -
-
-

+
+
+
+

Application Settings

@@ -67,16 +68,16 @@ templ Settings(ctx context.Context, data SettingsData) { Notifications - -
  • - -
  • + // + //
  • + // + //
  • @@ -435,6 +436,7 @@ templ Settings(ctx context.Context, data SettingsData) {

    +
    } @toggleNotificationFields() } diff --git a/components/user_management.templ b/components/user_management.templ index 3e4a2d0..0363417 100644 --- a/components/user_management.templ +++ b/components/user_management.templ @@ -7,17 +7,26 @@ import ( ) type UserManagementData struct { - Users []db.User - Roles []db.Role - ActiveTab string // "list", "create", or "edit" - EditUser *db.User - UserRoles []db.Role - ErrorMessage string + Users []db.User + Roles []db.Role + ActiveTab string // "list", "create", or "edit" + EditUser *db.User + UserRoles []db.Role + ErrorMessage string + SuccessMessage string } templ UserManagement(ctx context.Context, data UserManagementData) { @LayoutWithContext("User Management", ctx) { -
    + @UserManagementContent(data) + } +} + +// UserManagementContent is a partial component for HTMX to replace only the content +templ UserManagementContent(data UserManagementData) { + //
    +
    +

    @@ -25,38 +34,33 @@ templ UserManagement(ctx context.Context, data UserManagementData) { User Management

    - -
    - - - + if data.SuccessMessage != "" { + + } else { + + } if data.ErrorMessage != "" { } - - -
    + +
    + if data.ActiveTab == "list" || data.ActiveTab == "" { @userList(data.Users) } else if data.ActiveTab == "create" { @@ -65,7 +69,6 @@ templ UserManagement(ctx context.Context, data UserManagementData) { @userForm(data.EditUser, false, data.Roles) }
    -
    - } +
    } // User listing component @@ -149,17 +152,16 @@ templ userList(users []db.User) { - @@ -193,15 +195,14 @@ templ userForm(user *db.User, isNew bool, availableRoles []db.Role) { }

    - -
    -
    -
    -
    -
    -
    -
    -
    - if !isNew {

    Account Status

    -
    -
    } - if len(availableRoles) > 0 {

    Roles

    @@ -317,11 +312,11 @@ templ userForm(user *db.User, isNew bool, availableRoles []db.Role) { for _, role := range availableRoles {
    - }
    -
    -