diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 77e7aa4..c72243a 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -4,7 +4,8 @@
"Bash(dotnet build)",
"Bash(dotnet run:*)",
"Bash(powershell:*)",
- "Bash(dotnet script:*)"
+ "Bash(dotnet script:*)",
+ "Bash(taskkill:*)"
],
"deny": [],
"ask": []
diff --git a/Media.JoshHeaps.Net/Pages/Index.cshtml b/Media.JoshHeaps.Net/Pages/Index.cshtml
index ab81d29..670cd30 100644
--- a/Media.JoshHeaps.Net/Pages/Index.cshtml
+++ b/Media.JoshHeaps.Net/Pages/Index.cshtml
@@ -1,4 +1,4 @@
-@page
+@page "{handler?}"
@model Media.JoshHeaps.Net.Pages.IndexModel
@{
ViewData["Title"] = "Dashboard";
@@ -13,11 +13,6 @@
Welcome back, @Model.Dashboard?.Username!
-
Here's your account overview
-
-
-
-
Quick Actions
@if (Model.Dashboard?.EmailVerified == false)
{
@@ -25,29 +20,39 @@
Verify Email
}
+
Edit Profile
Logout
-
-
Upload Image
+
@@ -64,10 +69,10 @@
}
@media.CreatedAt.ToString("MMM d, yyyy")
-
@@ -79,6 +84,12 @@
+
+
+
@section Scripts {
+
}
\ No newline at end of file
diff --git a/Media.JoshHeaps.Net/Pages/Index.cshtml.cs b/Media.JoshHeaps.Net/Pages/Index.cshtml.cs
index ea74fb8..ee3ad29 100644
--- a/Media.JoshHeaps.Net/Pages/Index.cshtml.cs
+++ b/Media.JoshHeaps.Net/Pages/Index.cshtml.cs
@@ -17,6 +17,9 @@ namespace Media.JoshHeaps.Net.Pages
[BindProperty]
public string? Description { get; set; }
+ [BindProperty]
+ public long? MediaId { get; set; }
+
public string? UploadMessage { get; set; }
public bool UploadSuccess { get; set; }
@@ -39,6 +42,17 @@ namespace Media.JoshHeaps.Net.Pages
RequireAuthentication();
LoadUserSession();
+ Dashboard = await userService.GetUserDashboardAsync(UserId);
+ MediaItems = await _mediaService.GetUserMediaAsync(UserId, 0, 20);
+
+ return Page();
+ }
+
+ public async Task OnPostAddAsync()
+ {
+ RequireAuthentication();
+ LoadUserSession();
+
if (UploadedFile == null || UploadedFile.Length == 0)
{
UploadMessage = "Please select a file to upload.";
@@ -69,19 +83,19 @@ namespace Media.JoshHeaps.Net.Pages
}
}
- // Reload data
- Dashboard = await userService.GetUserDashboardAsync(UserId);
- MediaItems = await _mediaService.GetUserMediaAsync(UserId, 0, 20);
-
- return Page();
+ // Redirect to prevent form resubmission on refresh
+ return RedirectToPage();
}
- public async Task OnPostDeleteAsync(long mediaId)
+ public async Task OnPostDeleteAsync()
{
RequireAuthentication();
LoadUserSession();
- var success = await _mediaService.DeleteMediaAsync(mediaId, UserId);
+ if (MediaId.HasValue)
+ {
+ var success = await _mediaService.DeleteMediaAsync(MediaId.Value, UserId);
+ }
return RedirectToPage();
}
diff --git a/Media.JoshHeaps.Net/Pages/Profile.cshtml b/Media.JoshHeaps.Net/Pages/Profile.cshtml
new file mode 100644
index 0000000..18af864
--- /dev/null
+++ b/Media.JoshHeaps.Net/Pages/Profile.cshtml
@@ -0,0 +1,180 @@
+@page
+@model Media.JoshHeaps.Net.Pages.ProfileModel
+@{
+ ViewData["Title"] = "Profile Settings";
+ Layout = "_Layout";
+}
+
+@section Styles {
+
+
+}
+
+
+
← Back to Dashboard
+
+
+
Profile Information
+
+
+
@Model.Dashboard?.Username
+
+
+
+
@Model.Dashboard?.Email
+
+
+
+
+ @if (Model.Dashboard?.EmailVerified == true)
+ {
+ Verified
+ }
+ else
+ {
+ Unverified
+ }
+
+
+
+
+
+
Appearance
+
+
+ Dark Mode
+ Enable dark theme across the application
+
+
+
+
+
diff --git a/Media.JoshHeaps.Net/Pages/Profile.cshtml.cs b/Media.JoshHeaps.Net/Pages/Profile.cshtml.cs
new file mode 100644
index 0000000..c46ec25
--- /dev/null
+++ b/Media.JoshHeaps.Net/Pages/Profile.cshtml.cs
@@ -0,0 +1,22 @@
+using Media.JoshHeaps.Net.Models;
+using Media.JoshHeaps.Net.Services;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Media.JoshHeaps.Net.Pages
+{
+ public class ProfileModel(UserService userService) : AuthenticatedPageModel
+ {
+ public UserDashboard? Dashboard { get; set; }
+
+ public async Task OnGetAsync()
+ {
+ RequireAuthentication();
+ LoadUserSession();
+
+ // Load user dashboard data
+ Dashboard = await userService.GetUserDashboardAsync(UserId);
+
+ return Page();
+ }
+ }
+}
diff --git a/Media.JoshHeaps.Net/Pages/Shared/_Layout.cshtml b/Media.JoshHeaps.Net/Pages/Shared/_Layout.cshtml
index dff923b..b8b8a2a 100644
--- a/Media.JoshHeaps.Net/Pages/Shared/_Layout.cshtml
+++ b/Media.JoshHeaps.Net/Pages/Shared/_Layout.cshtml
@@ -9,6 +9,8 @@
@ViewData["Title"]
+
+
@await RenderSectionAsync("Styles", required: false)
@await RenderSectionAsync("Scripts", required: false)
diff --git a/Media.JoshHeaps.Net/Pages/_ViewImports.cshtml b/Media.JoshHeaps.Net/Pages/_ViewImports.cshtml
new file mode 100644
index 0000000..ac7c365
--- /dev/null
+++ b/Media.JoshHeaps.Net/Pages/_ViewImports.cshtml
@@ -0,0 +1,4 @@
+@using Media.JoshHeaps.Net
+@using Media.JoshHeaps.Net.Pages
+@namespace Media.JoshHeaps.Net.Pages
+@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
diff --git a/Media.JoshHeaps.Net/appsettings.json b/Media.JoshHeaps.Net/appsettings.json
index b2fa4e4..5c719a9 100644
--- a/Media.JoshHeaps.Net/appsettings.json
+++ b/Media.JoshHeaps.Net/appsettings.json
@@ -19,8 +19,5 @@
"MaxFileSizeMB": 10,
"AllowedImageTypes": ["image/jpeg", "image/png", "image/gif", "image/webp"],
"UploadPath": "App_Data/media"
- },
- "Encryption": {
- "Key": "***REMOVED***"
}
}
diff --git a/Media.JoshHeaps.Net/wwwroot/css/Home/page.css b/Media.JoshHeaps.Net/wwwroot/css/Home/page.css
index 04e4ca5..7ce6d83 100644
--- a/Media.JoshHeaps.Net/wwwroot/css/Home/page.css
+++ b/Media.JoshHeaps.Net/wwwroot/css/Home/page.css
@@ -5,23 +5,27 @@
}
.welcome-section {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- padding: 40px;
- border-radius: 12px;
- margin-bottom: 30px;
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ background: var(--bg-tertiary);
+ border: 1px solid var(--border-primary);
+ color: var(--text-primary);
+ padding: 32px 40px;
+ border-radius: 8px;
+ margin-bottom: 32px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
}
.welcome-section h1 {
- margin: 0 0 10px 0;
- font-size: 32px;
+ margin: 0;
+ font-size: 28px;
font-weight: 600;
+ letter-spacing: -0.5px;
}
.welcome-section p {
margin: 0;
- opacity: 0.9;
+ opacity: 0.8;
font-size: 16px;
}
@@ -33,26 +37,31 @@
}
.card {
- background: white;
- border-radius: 12px;
- padding: 30px;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-primary);
+ border-radius: 8px;
+ padding: 24px;
+ transition: border-color 0.2s ease;
+}
+
+.card:hover {
+ border-color: var(--border-secondary);
}
.card h2 {
margin: 0 0 20px 0;
- font-size: 20px;
+ font-size: 18px;
font-weight: 600;
- color: #333;
- border-bottom: 2px solid #667eea;
- padding-bottom: 10px;
+ color: var(--text-primary);
+ border-bottom: 1px solid var(--border-primary);
+ padding-bottom: 12px;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 12px 0;
- border-bottom: 1px solid #f0f0f0;
+ border-bottom: 1px solid var(--border-secondary);
}
.info-row:last-child {
@@ -61,78 +70,86 @@
.info-label {
font-weight: 500;
- color: #666;
+ color: var(--text-secondary);
}
.info-value {
- color: #333;
+ color: var(--text-primary);
font-weight: 500;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
- border-radius: 12px;
+ border-radius: 6px;
font-size: 12px;
font-weight: 600;
}
.status-verified {
- background: #d4edda;
- color: #155724;
+ background: rgba(63, 185, 80, 0.15);
+ color: var(--success);
}
.status-unverified {
- background: #fff3cd;
- color: #856404;
+ background: rgba(187, 128, 9, 0.15);
+ color: #d29922;
}
.quick-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
- margin-top: 20px;
+ align-content: space-evenly;
}
.btn {
- padding: 10px 20px;
+ padding: 8px 16px;
border-radius: 6px;
text-decoration: none;
font-weight: 500;
+ font-size: 14px;
display: inline-block;
- transition: all 0.2s;
+ transition: all 0.2s ease;
+ border: 1px solid transparent;
}
.btn-primary {
- background: #667eea;
- color: white;
+ background: var(--accent-primary);
+ color: var(--bg-primary);
+ border-color: var(--accent-primary);
}
.btn-primary:hover {
- background: #5568d3;
+ background: var(--accent-hover);
+ border-color: var(--accent-hover);
}
.btn-secondary {
- background: #6c757d;
- color: white;
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+ border-color: var(--border-primary);
}
.btn-secondary:hover {
- background: #5a6268;
+ background: var(--bg-hover);
+ border-color: var(--border-secondary);
}
.btn-danger {
- background: #dc3545;
- color: white;
+ background: transparent;
+ color: var(--danger);
+ border-color: var(--danger);
}
.btn-danger:hover {
- background: #c82333;
+ background: var(--danger);
+ color: var(--text-primary);
}
.empty-state {
text-align: center;
- color: #999;
+ color: var(--text-secondary);
padding: 20px;
font-style: italic;
}
diff --git a/Media.JoshHeaps.Net/wwwroot/css/auth.css b/Media.JoshHeaps.Net/wwwroot/css/auth.css
index 10e5a9c..e7c7115 100644
--- a/Media.JoshHeaps.Net/wwwroot/css/auth.css
+++ b/Media.JoshHeaps.Net/wwwroot/css/auth.css
@@ -1,29 +1,19 @@
/* Authentication Pages Styling */
-:root {
- --primary-color: #007bff;
- --primary-hover: #0056b3;
- --danger-color: #dc3545;
- --success-color: #28a745;
- --warning-color: #ffc107;
- --border-color: #dee2e6;
- --input-focus: #80bdff;
- --text-muted: #6c757d;
-}
-
.auth-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ background: var(--bg-primary);
padding: 20px;
}
.auth-card {
- background: white;
- border-radius: 12px;
- box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-primary);
+ border-radius: 8px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
padding: 40px;
width: 100%;
max-width: 450px;
@@ -31,18 +21,19 @@
.auth-header {
text-align: center;
- margin-bottom: 30px;
+ margin-bottom: 32px;
}
.auth-header h1 {
font-size: 28px;
font-weight: 600;
- color: #333;
- margin-bottom: 10px;
+ color: var(--text-primary);
+ margin-bottom: 8px;
+ letter-spacing: -0.5px;
}
.auth-header p {
- color: var(--text-muted);
+ color: var(--text-secondary);
font-size: 14px;
}
@@ -52,7 +43,7 @@
.form-label {
font-weight: 500;
- color: #333;
+ color: var(--text-primary);
margin-bottom: 8px;
display: block;
font-size: 14px;
@@ -61,28 +52,30 @@
.form-control {
width: 100%;
padding: 12px 16px;
- border: 1px solid var(--border-color);
+ border: 1px solid var(--border-primary);
border-radius: 6px;
font-size: 14px;
- transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.form-control:focus {
outline: none;
- border-color: var(--input-focus);
- box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
+ border-color: var(--accent-primary);
+ box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.15);
}
.form-control.is-invalid {
- border-color: var(--danger-color);
+ border-color: var(--danger);
}
.form-control.is-invalid:focus {
- box-shadow: 0 0 0 3px rgba(220, 53, 69, 0.1);
+ box-shadow: 0 0 0 3px rgba(248, 81, 73, 0.15);
}
.invalid-feedback {
- color: var(--danger-color);
+ color: var(--danger);
font-size: 13px;
margin-top: 5px;
display: none;
@@ -103,20 +96,21 @@
transform: translateY(-50%);
background: none;
border: none;
- color: var(--text-muted);
+ color: var(--text-secondary);
cursor: pointer;
padding: 4px 8px;
font-size: 14px;
+ transition: color 0.2s ease;
}
.password-toggle:hover {
- color: #333;
+ color: var(--text-primary);
}
.password-strength {
margin-top: 8px;
height: 4px;
- background: var(--border-color);
+ background: var(--bg-tertiary);
border-radius: 2px;
overflow: hidden;
display: none;
@@ -133,17 +127,17 @@
}
.password-strength-bar.weak {
- background-color: var(--danger-color);
+ background-color: var(--danger);
width: 33%;
}
.password-strength-bar.medium {
- background-color: var(--warning-color);
+ background-color: #d29922;
width: 66%;
}
.password-strength-bar.strong {
- background-color: var(--success-color);
+ background-color: var(--success);
width: 100%;
}
@@ -151,6 +145,7 @@
font-size: 12px;
margin-top: 4px;
display: none;
+ color: var(--text-secondary);
}
.password-strength-text.active {
@@ -167,11 +162,13 @@
margin-right: 8px;
width: 16px;
height: 16px;
+ accent-color: var(--accent-primary);
+ cursor: pointer;
}
.checkbox-wrapper label {
font-size: 14px;
- color: #333;
+ color: var(--text-primary);
margin: 0;
cursor: pointer;
}
@@ -179,22 +176,25 @@
.btn-primary {
width: 100%;
padding: 12px;
- background: var(--primary-color);
- color: white;
- border: none;
+ background: var(--accent-primary);
+ color: var(--bg-primary);
+ border: 1px solid var(--accent-primary);
border-radius: 6px;
- font-size: 16px;
+ font-size: 15px;
font-weight: 500;
cursor: pointer;
- transition: background-color 0.15s ease-in-out;
+ transition: all 0.2s ease;
}
.btn-primary:hover {
- background: var(--primary-hover);
+ background: var(--accent-hover);
+ border-color: var(--accent-hover);
}
.btn-primary:disabled {
- background: var(--text-muted);
+ background: var(--bg-tertiary);
+ border-color: var(--border-primary);
+ color: var(--text-secondary);
cursor: not-allowed;
}
@@ -202,22 +202,24 @@
text-align: center;
margin-top: 24px;
padding-top: 24px;
- border-top: 1px solid var(--border-color);
+ border-top: 1px solid var(--border-primary);
}
.auth-footer p {
- color: var(--text-muted);
+ color: var(--text-secondary);
font-size: 14px;
margin: 0;
}
.auth-footer a {
- color: var(--primary-color);
+ color: var(--accent-primary);
text-decoration: none;
font-weight: 500;
+ transition: color 0.2s ease;
}
.auth-footer a:hover {
+ color: var(--accent-hover);
text-decoration: underline;
}
@@ -229,29 +231,29 @@
}
.alert-danger {
- background-color: #f8d7da;
- color: #721c24;
- border: 1px solid #f5c6cb;
+ background-color: rgba(248, 81, 73, 0.15);
+ color: var(--danger);
+ border: 1px solid rgba(248, 81, 73, 0.3);
}
.alert-success {
- background-color: #d4edda;
- color: #155724;
- border: 1px solid #c3e6cb;
+ background-color: rgba(63, 185, 80, 0.15);
+ color: var(--success);
+ border: 1px solid rgba(63, 185, 80, 0.3);
}
.alert-warning {
- background-color: #fff3cd;
- color: #856404;
- border: 1px solid #ffeaa7;
+ background-color: rgba(187, 128, 9, 0.15);
+ color: #d29922;
+ border: 1px solid rgba(187, 128, 9, 0.3);
}
.spinner {
display: inline-block;
width: 16px;
height: 16px;
- border: 2px solid rgba(255, 255, 255, 0.3);
- border-top-color: white;
+ border: 2px solid rgba(88, 166, 255, 0.3);
+ border-top-color: var(--accent-primary);
border-radius: 50%;
animation: spin 0.6s linear infinite;
margin-right: 8px;
diff --git a/Media.JoshHeaps.Net/wwwroot/css/gallery.css b/Media.JoshHeaps.Net/wwwroot/css/gallery.css
index 0a0c53d..7d93f75 100644
--- a/Media.JoshHeaps.Net/wwwroot/css/gallery.css
+++ b/Media.JoshHeaps.Net/wwwroot/css/gallery.css
@@ -1,23 +1,129 @@
+/* Floating Add Files Button */
+.floating-add-btn {
+ position: fixed;
+ bottom: 30px;
+ right: 30px;
+ width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ background: var(--accent-primary);
+ color: var(--bg-primary);
+ border: 1px solid var(--accent-primary);
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
+ cursor: pointer;
+ z-index: 1000;
+ transition: all 0.2s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 28px;
+ line-height: 1;
+ font-weight: 300;
+}
+
+.floating-add-btn:hover {
+ transform: scale(1.05);
+ background: var(--accent-hover);
+ border-color: var(--accent-hover);
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
+}
+
+.floating-add-btn:active {
+ transform: scale(0.98);
+}
+
+.floating-add-btn .btn-icon {
+ transition: transform 0.3s ease;
+}
+
+.floating-add-btn.active .btn-icon {
+ transform: rotate(45deg);
+}
+
+/* Upload Form Styles */
+#upload-form-card {
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 1001;
+ max-width: 500px;
+ width: 90%;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
+}
+
+/* Upload Progress Bar */
+#upload-progress {
+ margin: 15px 0;
+}
+
+.progress-bar {
+ width: 100%;
+ height: 8px;
+ background: var(--bg-tertiary);
+ border-radius: 4px;
+ overflow: hidden;
+ margin-bottom: 10px;
+}
+
+.progress-fill {
+ height: 100%;
+ background: var(--accent-primary);
+ transition: width 0.3s ease;
+ width: 0%;
+}
+
+.progress-text {
+ text-align: center;
+ font-size: 13px;
+ color: var(--text-secondary);
+ font-weight: 500;
+}
+
+.upload-form-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+}
+
+.close-upload-form {
+ background: transparent;
+ border: none;
+ font-size: 28px;
+ color: var(--text-secondary);
+ cursor: pointer;
+ padding: 0;
+ width: 32px;
+ height: 32px;
+ line-height: 1;
+ transition: color 0.2s ease;
+}
+
+.close-upload-form:hover {
+ color: var(--text-primary);
+}
+
/* Gallery Grid Layout */
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
- gap: 20px;
+ gap: 16px;
padding: 20px 0;
}
.gallery-item {
position: relative;
- background: #fff;
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-primary);
border-radius: 8px;
overflow: hidden;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- transition: transform 0.2s, box-shadow 0.2s;
+ transition: all 0.2s ease;
}
.gallery-item:hover {
- transform: translateY(-4px);
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+ transform: translateY(-2px);
+ border-color: var(--border-secondary);
}
.gallery-item img {
@@ -26,91 +132,137 @@
object-fit: cover;
display: block;
cursor: pointer;
+ transition: opacity 0.2s ease;
+}
+
+.gallery-item img:hover {
+ opacity: 0.9;
}
.gallery-item-description {
- padding: 10px;
- font-size: 14px;
- color: #555;
- background: #f9f9f9;
- border-top: 1px solid #eee;
+ padding: 12px;
+ font-size: 13px;
+ color: var(--text-secondary);
+ background: var(--bg-secondary);
+ border-top: 1px solid var(--border-primary);
+ line-height: 1.5;
}
.gallery-item-info {
display: flex;
justify-content: space-between;
align-items: center;
- padding: 10px;
- background: #fff;
- border-top: 1px solid #eee;
+ padding: 10px 12px;
+ background: var(--bg-tertiary);
+ border-top: 1px solid var(--border-primary);
}
.gallery-item-date {
font-size: 12px;
- color: #888;
+ color: var(--text-secondary);
}
.btn-delete {
- background: #dc3545;
- color: white;
- border: none;
- padding: 5px 12px;
+ background: transparent;
+ color: var(--danger);
+ border: 1px solid var(--danger);
+ padding: 4px 10px;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
- transition: background 0.2s;
+ transition: all 0.2s ease;
+ font-weight: 500;
}
.btn-delete:hover {
- background: #c82333;
+ background: var(--danger);
+ color: var(--text-primary);
}
/* Upload Form Styles */
.form-group {
- margin-bottom: 15px;
+ margin-bottom: 16px;
}
.form-group label {
display: block;
- margin-bottom: 5px;
+ margin-bottom: 8px;
font-weight: 500;
- color: #333;
+ color: var(--text-primary);
+ font-size: 14px;
}
.form-group input[type="file"] {
width: 100%;
- padding: 8px;
- border: 2px dashed #ddd;
- border-radius: 4px;
+ padding: 12px;
+ border: 1px dashed var(--border-primary);
+ border-radius: 6px;
cursor: pointer;
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+ font-size: 14px;
+ transition: border-color 0.2s ease;
+}
+
+.form-group input[type="file"]:hover {
+ border-color: var(--accent-primary);
+}
+
+.form-group input[type="file"]::file-selector-button {
+ padding: 8px 16px;
+ margin-right: 12px;
+ background: var(--accent-primary);
+ color: var(--bg-primary);
+ border: 1px solid var(--accent-primary);
+ border-radius: 4px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.form-group input[type="file"]::file-selector-button:hover {
+ background: var(--accent-hover);
+ border-color: var(--accent-hover);
}
.form-group textarea {
width: 100%;
padding: 10px;
- border: 1px solid #ddd;
- border-radius: 4px;
+ border: 1px solid var(--border-primary);
+ border-radius: 6px;
font-family: inherit;
resize: vertical;
+ background: var(--bg-tertiary);
+ color: var(--text-primary);
+ font-size: 14px;
+ line-height: 1.5;
+ transition: border-color 0.2s ease;
+}
+
+.form-group textarea:focus {
+ outline: none;
+ border-color: var(--accent-primary);
}
/* Alert Messages */
.alert {
padding: 12px 16px;
- border-radius: 4px;
- margin-bottom: 15px;
+ border-radius: 6px;
+ margin-bottom: 16px;
+ font-size: 14px;
}
.alert-success {
- background: #d4edda;
- color: #155724;
- border: 1px solid #c3e6cb;
+ background: rgba(63, 185, 80, 0.15);
+ color: var(--success);
+ border: 1px solid rgba(63, 185, 80, 0.3);
}
.alert-error {
- background: #f8d7da;
- color: #721c24;
- border: 1px solid #f5c6cb;
+ background: rgba(248, 81, 73, 0.15);
+ color: var(--danger);
+ border: 1px solid rgba(248, 81, 73, 0.3);
}
/* Image Modal */
@@ -132,7 +284,8 @@
left: 0;
right: 0;
bottom: 0;
- background: rgba(0, 0, 0, 0.8);
+ background: rgba(0, 0, 0, 0.85);
+ backdrop-filter: blur(4px);
}
.modal-content {
@@ -147,25 +300,27 @@
max-height: 90vh;
display: block;
border-radius: 8px;
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
}
.modal-close {
position: absolute;
- top: -40px;
+ top: -50px;
right: 0;
background: transparent;
border: none;
- color: white;
- font-size: 36px;
+ color: var(--text-primary);
+ font-size: 32px;
cursor: pointer;
padding: 0;
width: 40px;
height: 40px;
- line-height: 36px;
+ line-height: 32px;
+ transition: color 0.2s ease;
}
.modal-close:hover {
- color: #ccc;
+ color: var(--text-secondary);
}
/* Responsive Design */
diff --git a/Media.JoshHeaps.Net/wwwroot/css/site.css b/Media.JoshHeaps.Net/wwwroot/css/site.css
index e69de29..e60d954 100644
--- a/Media.JoshHeaps.Net/wwwroot/css/site.css
+++ b/Media.JoshHeaps.Net/wwwroot/css/site.css
@@ -0,0 +1,81 @@
+/* Theme Variables */
+:root {
+ /* Dark Mode (Default) */
+ --bg-primary: #0d1117;
+ --bg-secondary: #161b22;
+ --bg-tertiary: #1f2937;
+ --bg-hover: #252d3a;
+ --text-primary: #e6edf3;
+ --text-secondary: #8b949e;
+ --border-primary: #30363d;
+ --border-secondary: #21262d;
+ --accent-primary: #58a6ff;
+ --accent-hover: #4a93e3;
+ --danger: #f85149;
+ --danger-hover: #da3633;
+ --success: #3fb950;
+}
+
+/* Light Mode */
+[data-theme="light"] {
+ --bg-primary: #ffffff;
+ --bg-secondary: #f6f8fa;
+ --bg-tertiary: #E2EFFF;
+ --bg-hover: #f3f4f6;
+ --text-primary: #24292f;
+ --text-secondary: #57606a;
+ --border-primary: #d0d7de;
+ --border-secondary: #e5e7eb;
+ --accent-primary: #0969da;
+ --accent-hover: #0550ae;
+ --danger: #cf222e;
+ --danger-hover: #a40e26;
+ --success: #1a7f37;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+html, body {
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
+ line-height: 1.6;
+ min-height: 100vh;
+}
+
+body {
+ margin: 0;
+ padding: 0;
+}
+
+a {
+ color: var(--accent-primary);
+ text-decoration: none;
+ transition: color 0.2s ease;
+}
+
+a:hover {
+ color: var(--accent-hover);
+}
+
+/* Scrollbar Styling */
+::-webkit-scrollbar {
+ width: 12px;
+}
+
+::-webkit-scrollbar-track {
+ background: var(--bg-primary);
+}
+
+::-webkit-scrollbar-thumb {
+ background: var(--border-primary);
+ border-radius: 6px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background: var(--bg-hover);
+}
diff --git a/Media.JoshHeaps.Net/wwwroot/js/gallery.js b/Media.JoshHeaps.Net/wwwroot/js/gallery.js
index fddbe4c..284d215 100644
--- a/Media.JoshHeaps.Net/wwwroot/js/gallery.js
+++ b/Media.JoshHeaps.Net/wwwroot/js/gallery.js
@@ -1,151 +1,200 @@
-// Lazy loading for gallery images using Intersection Observer
-let currentOffset = 20; // Initial load was 20 items
-let isLoading = false;
-let hasMore = true;
+// Wait for DOM to be fully loaded
+document.addEventListener('DOMContentLoaded', function() {
+ // Upload form toggle functionality
+ const addFilesBtn = document.getElementById('add-files-btn');
+ const uploadFormCard = document.getElementById('upload-form-card');
+ const closeUploadBtn = document.getElementById('close-upload-btn');
-// Create intersection observer for lazy loading
-const loadingElement = document.getElementById('loading');
-const galleryElement = document.getElementById('gallery');
-
-if (loadingElement && galleryElement) {
- const observer = new IntersectionObserver((entries) => {
- entries.forEach(entry => {
- if (entry.isIntersecting && !isLoading && hasMore) {
- loadMoreImages();
+ if (addFilesBtn && uploadFormCard) {
+ addFilesBtn.addEventListener('click', () => {
+ if (addFilesBtn.classList.contains('active')) {
+ uploadFormCard.style.display = 'none';
+ addFilesBtn.classList.remove('active');
+ }
+ else {
+ uploadFormCard.style.display = 'block';
+ addFilesBtn.classList.add('active');
}
});
- }, {
- rootMargin: '200px' // Start loading 200px before the element comes into view
- });
- observer.observe(loadingElement);
-}
-
-async function loadMoreImages() {
- if (isLoading || !hasMore) return;
-
- isLoading = true;
- loadingElement.style.display = 'block';
-
- try {
- const response = await fetch(`/api/media/load?offset=${currentOffset}&limit=20`);
-
- if (!response.ok) {
- throw new Error('Failed to load images');
+ if (closeUploadBtn) {
+ closeUploadBtn.addEventListener('click', () => {
+ uploadFormCard.style.display = 'none';
+ addFilesBtn.classList.remove('active');
+ });
}
- const mediaItems = await response.json();
-
- if (mediaItems.length === 0) {
- hasMore = false;
- loadingElement.style.display = 'none';
- return;
- }
-
- // Add new images to gallery
- mediaItems.forEach(media => {
- const galleryItem = createGalleryItem(media);
- galleryElement.appendChild(galleryItem);
+ // Close on Escape key
+ document.addEventListener('keydown', (e) => {
+ if (e.key === 'Escape' && uploadFormCard.style.display === 'block') {
+ uploadFormCard.style.display = 'none';
+ addFilesBtn.classList.remove('active');
+ }
});
- currentOffset += mediaItems.length;
- } catch (error) {
- console.error('Error loading images:', error);
- loadingElement.innerHTML = 'Failed to load more images.';
- } finally {
- isLoading = false;
- if (hasMore) {
- loadingElement.style.display = 'none';
+ // Close when clicking outside the form
+ document.addEventListener('click', (e) => {
+ if (uploadFormCard.style.display === 'block' &&
+ !uploadFormCard.contains(e.target) &&
+ !addFilesBtn.contains(e.target)) {
+ uploadFormCard.style.display = 'none';
+ addFilesBtn.classList.remove('active');
+ }
+ });
+ }
+
+ // Lazy loading for gallery images using Intersection Observer
+ let currentOffset = 20; // Initial load was 20 items
+ let isLoading = false;
+ let hasMore = true;
+
+ // Create intersection observer for lazy loading
+ const loadingElement = document.getElementById('loading');
+ const galleryElement = document.getElementById('gallery');
+
+ if (loadingElement && galleryElement) {
+ const observer = new IntersectionObserver((entries) => {
+ entries.forEach(entry => {
+ if (entry.isIntersecting && !isLoading && hasMore) {
+ loadMoreImages();
+ }
+ });
+ }, {
+ rootMargin: '200px' // Start loading 200px before the element comes into view
+ });
+
+ observer.observe(loadingElement);
+ }
+
+ async function loadMoreImages() {
+ if (isLoading || !hasMore) return;
+
+ isLoading = true;
+ loadingElement.style.display = 'block';
+
+ try {
+ const response = await fetch(`/api/media/load?offset=${currentOffset}&limit=20`);
+
+ if (!response.ok) {
+ throw new Error('Failed to load images');
+ }
+
+ const mediaItems = await response.json();
+
+ if (mediaItems.length === 0) {
+ hasMore = false;
+ loadingElement.style.display = 'none';
+ return;
+ }
+
+ // Add new images to gallery
+ mediaItems.forEach(media => {
+ const galleryItem = createGalleryItem(media);
+ galleryElement.appendChild(galleryItem);
+ });
+
+ currentOffset += mediaItems.length;
+ } catch (error) {
+ console.error('Error loading images:', error);
+ loadingElement.innerHTML = 'Failed to load more images.';
+ } finally {
+ isLoading = false;
+ if (hasMore) {
+ loadingElement.style.display = 'none';
+ }
}
}
-}
-function createGalleryItem(media) {
- const item = document.createElement('div');
- item.className = 'gallery-item';
- item.setAttribute('data-media-id', media.id);
+ function createGalleryItem(media) {
+ const item = document.createElement('div');
+ item.className = 'gallery-item';
+ item.setAttribute('data-media-id', media.id);
- const img = document.createElement('img');
- img.src = `/api/media/image/${media.id}`;
- img.alt = media.fileName;
- img.loading = 'lazy';
- item.appendChild(img);
+ const img = document.createElement('img');
+ img.src = `/api/media/image/${media.id}`;
+ img.alt = media.fileName;
+ img.loading = 'lazy';
+ item.appendChild(img);
- if (media.description) {
- const desc = document.createElement('div');
- desc.className = 'gallery-item-description';
- desc.textContent = media.description;
- item.appendChild(desc);
+ if (media.description) {
+ const desc = document.createElement('div');
+ desc.className = 'gallery-item-description';
+ desc.textContent = media.description;
+ item.appendChild(desc);
+ }
+
+ const info = document.createElement('div');
+ info.className = 'gallery-item-info';
+
+ const date = document.createElement('span');
+ date.className = 'gallery-item-date';
+ const createdDate = new Date(media.createdAt);
+ date.textContent = createdDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
+ info.appendChild(date);
+
+ const form = document.createElement('form');
+ form.method = 'post';
+ form.action = '?handler=Delete'; // Relative to current page
+ form.style.display = 'inline';
+
+ // Add anti-forgery token
+ const tokenInput = document.querySelector('input[name="__RequestVerificationToken"]');
+ if (tokenInput) {
+ const tokenClone = tokenInput.cloneNode(true);
+ form.appendChild(tokenClone);
+ }
+
+ const hiddenInput = document.createElement('input');
+ hiddenInput.type = 'hidden';
+ hiddenInput.name = 'MediaId'; // Match C# property name exactly
+ hiddenInput.value = media.id;
+ form.appendChild(hiddenInput);
+
+ const deleteBtn = document.createElement('button');
+ deleteBtn.type = 'submit';
+ deleteBtn.className = 'btn-delete';
+ deleteBtn.textContent = 'Delete';
+ deleteBtn.onclick = function(e) {
+ if (!confirm('Are you sure you want to delete this image?')) {
+ e.preventDefault();
+ return false;
+ }
+ };
+ form.appendChild(deleteBtn);
+
+ info.appendChild(form);
+ item.appendChild(info);
+
+ return item;
}
- const info = document.createElement('div');
- info.className = 'gallery-item-info';
-
- const date = document.createElement('span');
- date.className = 'gallery-item-date';
- const createdDate = new Date(media.createdAt);
- date.textContent = createdDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
- info.appendChild(date);
-
- const form = document.createElement('form');
- form.method = 'post';
- form.action = '/?handler=Delete';
- form.style.display = 'inline';
-
- // Add anti-forgery token
- const tokenInput = document.querySelector('input[name="__RequestVerificationToken"]');
- if (tokenInput) {
- const tokenClone = tokenInput.cloneNode(true);
- form.appendChild(tokenClone);
- }
-
- const hiddenInput = document.createElement('input');
- hiddenInput.type = 'hidden';
- hiddenInput.name = 'mediaId';
- hiddenInput.value = media.id;
- form.appendChild(hiddenInput);
-
- const deleteBtn = document.createElement('button');
- deleteBtn.type = 'submit';
- deleteBtn.className = 'btn-delete';
- deleteBtn.textContent = 'Delete';
- deleteBtn.onclick = function(e) {
- return confirm('Are you sure you want to delete this image?');
- };
- form.appendChild(deleteBtn);
-
- info.appendChild(form);
- item.appendChild(info);
-
- return item;
-}
-
-// Optional: Add image preview on click
-galleryElement?.addEventListener('click', (e) => {
- if (e.target.tagName === 'IMG') {
- const modal = createImageModal(e.target.src);
- document.body.appendChild(modal);
- }
-});
-
-function createImageModal(imageSrc) {
- const modal = document.createElement('div');
- modal.className = 'image-modal';
- modal.innerHTML = `
-
-
-

-
-
- `;
-
- modal.addEventListener('click', (e) => {
- if (e.target.classList.contains('modal-backdrop') ||
- e.target.classList.contains('modal-close') ||
- e.target.classList.contains('image-modal')) {
- modal.remove();
+ // Optional: Add image preview on click
+ galleryElement?.addEventListener('click', (e) => {
+ if (e.target.tagName === 'IMG') {
+ const modal = createImageModal(e.target.src);
+ document.body.appendChild(modal);
}
});
- return modal;
-}
+ function createImageModal(imageSrc) {
+ const modal = document.createElement('div');
+ modal.className = 'image-modal';
+ modal.innerHTML = `
+
+
+

+
+
+ `;
+
+ modal.addEventListener('click', (e) => {
+ if (e.target.classList.contains('modal-backdrop') ||
+ e.target.classList.contains('modal-close') ||
+ e.target.classList.contains('image-modal')) {
+ modal.remove();
+ }
+ });
+
+ return modal;
+ }
+}); // End of DOMContentLoaded
diff --git a/Media.JoshHeaps.Net/wwwroot/js/theme.js b/Media.JoshHeaps.Net/wwwroot/js/theme.js
new file mode 100644
index 0000000..b59e698
--- /dev/null
+++ b/Media.JoshHeaps.Net/wwwroot/js/theme.js
@@ -0,0 +1,38 @@
+// Theme Toggle Functionality
+(function() {
+ const THEME_KEY = 'theme-preference';
+
+ // Initialize theme on page load
+ function initTheme() {
+ const savedTheme = localStorage.getItem(THEME_KEY);
+ const theme = savedTheme || 'light'; // Default to light
+
+ document.documentElement.setAttribute('data-theme', theme);
+
+ // Update toggle if it exists
+ const toggle = document.getElementById('theme-toggle');
+ if (toggle) {
+ toggle.checked = theme === 'dark';
+ }
+ }
+
+ // Toggle theme
+ function toggleTheme() {
+ const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
+
+ document.documentElement.setAttribute('data-theme', newTheme);
+ localStorage.setItem(THEME_KEY, newTheme);
+ }
+
+ // Initialize immediately (before DOMContentLoaded to prevent flash)
+ initTheme();
+
+ // Set up toggle listener when DOM is ready
+ document.addEventListener('DOMContentLoaded', function() {
+ const toggle = document.getElementById('theme-toggle');
+ if (toggle) {
+ toggle.addEventListener('change', toggleTheme);
+ }
+ });
+})();
diff --git a/Media.JoshHeaps.Net/wwwroot/js/upload.js b/Media.JoshHeaps.Net/wwwroot/js/upload.js
new file mode 100644
index 0000000..710cf37
--- /dev/null
+++ b/Media.JoshHeaps.Net/wwwroot/js/upload.js
@@ -0,0 +1,106 @@
+// Handle multiple file uploads sequentially
+document.addEventListener('DOMContentLoaded', function() {
+ const uploadForm = document.getElementById('upload-form');
+ const fileInput = document.getElementById('UploadedFile');
+ const descriptionInput = document.getElementById('Description');
+ const uploadBtn = document.getElementById('upload-btn');
+ const progressContainer = document.getElementById('upload-progress');
+ const progressFill = document.getElementById('progress-fill');
+ const progressText = document.getElementById('progress-text');
+ const galleryElement = document.getElementById('gallery');
+
+ if (!uploadForm) return;
+
+ uploadForm.addEventListener('submit', async function(e) {
+ e.preventDefault();
+
+ const files = fileInput.files;
+ if (!files || files.length === 0) {
+ alert('Please select at least one file');
+ return;
+ }
+
+ // Validate file sizes
+ const maxSize = 10 * 1024 * 1024; // 10MB
+ for (let file of files) {
+ if (file.size > maxSize) {
+ alert(`File "${file.name}" exceeds 10MB limit`);
+ return;
+ }
+ }
+
+ // Disable form during upload
+ uploadBtn.disabled = true;
+ fileInput.disabled = true;
+ descriptionInput.disabled = true;
+ progressContainer.style.display = 'block';
+
+ const description = descriptionInput.value;
+ let successCount = 0;
+ let failCount = 0;
+
+ // Upload files sequentially
+ for (let i = 0; i < files.length; i++) {
+ const file = files[i];
+ const progress = Math.round(((i + 1) / files.length) * 100);
+
+ progressFill.style.width = `${progress}%`;
+ progressText.textContent = `Uploading ${i + 1} of ${files.length}: ${file.name}`;
+
+ try {
+ const success = await uploadSingleFile(file, description);
+ if (success) {
+ successCount++;
+ } else {
+ failCount++;
+ }
+ } catch (error) {
+ console.error(`Failed to upload ${file.name}:`, error);
+ failCount++;
+ }
+ }
+
+ // Reset form
+ uploadBtn.disabled = false;
+ fileInput.disabled = false;
+ descriptionInput.disabled = false;
+ progressContainer.style.display = 'none';
+ progressFill.style.width = '0%';
+
+ // Show results
+ if (successCount > 0 && failCount === 0) { }
+ else if (successCount > 0 && failCount > 0) {
+ alert(`Uploaded ${successCount} image(s), but ${failCount} failed.`);
+ } else {
+ alert(`Failed to upload all ${failCount} image(s).`);
+ }
+
+ // Reset form and reload page
+ uploadForm.reset();
+ window.location.reload();
+ });
+
+ async function uploadSingleFile(file, description) {
+ const formData = new FormData();
+ formData.append('UploadedFile', file);
+ if (description) {
+ formData.append('Description', description);
+ }
+
+ // Get anti-forgery token
+ const token = document.querySelector('input[name="__RequestVerificationToken"]').value;
+ formData.append('__RequestVerificationToken', token);
+
+ try {
+ const response = await fetch('/?handler=Add', {
+ method: 'POST',
+ body: formData
+ });
+
+ return response.ok;
+ } catch (error) {
+ console.error('Upload error:', error);
+ return false;
+ }
+ }
+});