mbkbucket 1.2.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- // Type definitions for mbkbucket 1.1.0
1
+ // Type definitions for mbkbucket 1.2.1
2
2
  // Project: https://github.com/MIbnEKhalid/mbkbucket
3
3
  // Definitions by: Muhammad Bin Khalid <https://github.com/MIbnEKhalid">
4
4
 
package/index.js CHANGED
@@ -40,8 +40,21 @@ const generalLimiter = rateLimit({
40
40
  server.use(express.json());
41
41
  server.use(express.urlencoded({ extended: true }));
42
42
 
43
- // Serve static assets from /public (CSS/JS/images)
44
- server.use(express.static(path.join(__dirname, 'public'), { maxAge: '365d', etag: true }));
43
+ // Serve only specific static assets (limit exposure): CSS and JS used by the dashboard
44
+ // This replaces serving the entire /public directory.
45
+ const staticMaxAgeSeconds = 90 * 24 * 60 * 60; // 90 days
46
+ server.get('/mbkbucket/bucketadmin.css', (req, res) => {
47
+ res.set('Cache-Control', `public, max-age=${staticMaxAgeSeconds}, immutable`);
48
+ res.sendFile(path.join(__dirname, 'public', 'bucketadmin.css'), (err) => {
49
+ if (err) res.status(err.status || 404).end();
50
+ });
51
+ });
52
+ server.get('/mbkbucket/bucketadmin.js', (req, res) => {
53
+ res.set('Cache-Control', `public, max-age=${staticMaxAgeSeconds}, immutable`);
54
+ res.sendFile(path.join(__dirname, 'public', 'bucketadmin.js'), (err) => {
55
+ if (err) res.status(err.status || 404).end();
56
+ });
57
+ });
45
58
 
46
59
  // Configure Handlebars (single setup)
47
60
  server.engine("handlebars", engine({
package/lib/bucket.js CHANGED
@@ -18,7 +18,7 @@ fs.mkdirSync(CHUNKS_DIR, { recursive: true });
18
18
  // Key/prefix helpers are provided by `s3.js`: `ensurePrefix`, `ensureKeyHasAppPrefix`, `getAppName`.
19
19
 
20
20
  router.get('/mbkbucket', validateSessionAndRole('SuperAdmin'), async (req, res) => {
21
- renderPage(req, res, "bucket.handlebars", false, { page: "Admin Bucket", bucketvar: mbkautheVar?.bucket, message: req.query.message, error: req.query.error });
21
+ renderPage(req, res, "bucket.handlebars", false, { page: "Admin Bucket", bucketvar: mbkautheVar?.bucket, APP_NAME: mbkautheVar?.APP_NAME, message: req.query.message, error: req.query.error });
22
22
  });
23
23
 
24
24
  router.get(['/mbkbucket/info', "/mbkbucket/i"], validateSessionAndRole('SuperAdmin'), async (req, res) => {
@@ -28,7 +28,7 @@ router.get(['/mbkbucket/info', "/mbkbucket/i"], validateSessionAndRole('SuperAdm
28
28
  } catch (err) {
29
29
  console.error("[mbkauthe] Error fetching package-lock.json:", err);
30
30
  }
31
- renderPage(req, res, "info.handlebars", false, { page: "MBK Bucket Info", appVersion: appVersion, latestVersion, APP_NAME: mbkautheVar.APP_NAME });
31
+ renderPage(req, res, "info.handlebars", false, { page: "MBK Bucket Info", appVersion, latestVersion, APP_NAME: mbkautheVar?.APP_NAME });
32
32
  });
33
33
 
34
34
  // API endpoint for fetching files
@@ -45,11 +45,20 @@ router.get('/mbkbucket/api/files', validateSessionAndRole('SuperAdmin'), async (
45
45
  }
46
46
 
47
47
  try {
48
- const result = await listfiles(effectivePrefix);
49
- let files = result.Contents || [];
48
+ let files = [];
49
+ let continuationToken = undefined;
50
50
  let usedPrefix = effectivePrefix;
51
51
  let legacyFallback = false;
52
52
 
53
+ // Fetch all files recursively (handling S3 pagination)
54
+ do {
55
+ const result = await listfiles(effectivePrefix, { continuationToken });
56
+ if (result.Contents) {
57
+ files = files.concat(result.Contents);
58
+ }
59
+ continuationToken = result.nextToken;
60
+ } while (continuationToken);
61
+
53
62
  // If no files found under the app prefixed path, try root as a legacy fallback
54
63
  if (!files.length) {
55
64
  try {
@@ -69,8 +78,7 @@ router.get('/mbkbucket/api/files', validateSessionAndRole('SuperAdmin'), async (
69
78
  if (search.trim()) {
70
79
  const searchLower = search.toLowerCase();
71
80
  files = files.filter(file => {
72
- const fileName = file.Key.split('/').pop();
73
- return fileName.toLowerCase().includes(searchLower);
81
+ return file.Key.toLowerCase().includes(searchLower);
74
82
  });
75
83
  }
76
84
 
package/lib/s3.js CHANGED
@@ -216,13 +216,14 @@ export async function uploadFile(key, fileBuffer, contentType, options = {}) {
216
216
 
217
217
  // Download file with enhanced features
218
218
  export async function downloadFile(key, options = {}) {
219
+ let keyToDownload;
219
220
  try {
220
221
  if (!key) {
221
222
  throw new Error('Key is required');
222
223
  }
223
224
 
224
225
  // Ensure key is app-prefixed
225
- const keyToDownload = ensureKeyHasAppPrefix(key);
226
+ keyToDownload = ensureKeyHasAppPrefix(key);
226
227
 
227
228
  const {
228
229
  range = null,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mbkbucket",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "S3 Bucket manager for mbktech.org apps",
5
5
  "author": "Muhammad Bin Khalid",
6
6
  "homepage": "https://github.com/MIbnEKhalid/mbkbucket#readme",
@@ -19,7 +19,7 @@
19
19
  "express": "^4.22.1",
20
20
  "express-handlebars": "^8.0.3",
21
21
  "express-rate-limit": "^7.0.1",
22
- "mbkauthe": "^4.3.0",
22
+ "mbkauthe": "latest",
23
23
  "multer": "^2.0.2",
24
24
  "pg": "^8.16.3"
25
25
  },
@@ -48,7 +48,7 @@
48
48
  background: linear-gradient(135deg, var(--primary-color-ml) 0%, var(--primary-dark-color-ml) 100%);
49
49
  color: white;
50
50
  border: none;
51
- margin-bottom: 2rem;
51
+ margin-bottom: 1rem;
52
52
  }
53
53
 
54
54
  .stats-content {
@@ -121,6 +121,57 @@
121
121
  gap: 0.5rem;
122
122
  }
123
123
 
124
+ /* Upload Drop Zone */
125
+ .upload-drop-zone {
126
+ border: 2px dashed var(--gray-color-ml);
127
+ border-radius: var(--border-radius-lg-ml);
128
+ background-color: #f8fafc;
129
+ min-height: 160px;
130
+ display: flex;
131
+ align-items: center;
132
+ justify-content: center;
133
+ cursor: pointer;
134
+ text-align: center;
135
+ transition: all 0.2s ease;
136
+ position: relative;
137
+ overflow: hidden;
138
+ }
139
+
140
+ .upload-drop-zone:hover,
141
+ .upload-drop-zone.dragover {
142
+ border-color: var(--primary-color-ml);
143
+ background-color: #f0fdfa;
144
+ }
145
+
146
+ .drop-zone-content {
147
+ pointer-events: none;
148
+ padding: 2rem;
149
+ }
150
+
151
+ .drop-zone-content i {
152
+ font-size: 3rem;
153
+ color: var(--primary-color-ml);
154
+ margin-bottom: 1rem;
155
+ opacity: 0.8;
156
+ transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275);
157
+ }
158
+
159
+ .upload-drop-zone:hover .drop-zone-content i {
160
+ opacity: 1;
161
+ }
162
+
163
+ .drop-text {
164
+ font-weight: 600;
165
+ font-size: 1.1rem;
166
+ color: var(--dark-color-ml);
167
+ margin-bottom: 0.5rem;
168
+ }
169
+
170
+ .drop-hint {
171
+ font-size: 0.85rem;
172
+ color: var(--gray-color-ml);
173
+ }
174
+
124
175
  /* Button styling - Modern hover effects */
125
176
  .btn {
126
177
  background: var(--primary-color-ml);
@@ -347,64 +398,74 @@
347
398
  /* Table styling - Modern clean design */
348
399
  .table-responsive {
349
400
  overflow-x: auto;
350
- box-shadow: inset 0 0 0 1px var(--gray-light-color-ml);
401
+ border-radius: var(--border-radius-lg-ml);
402
+ box-shadow: none;
351
403
  }
352
404
 
353
405
  .table {
354
406
  width: 100%;
355
407
  margin-bottom: 0;
356
408
  background: white;
357
- border-collapse: collapse;
409
+ border-collapse: separate;
410
+ border-spacing: 0;
358
411
  }
359
412
 
360
413
  .table thead {
361
- background: linear-gradient(135deg, var(--dark-color-ml) 0%, #2d3748 100%);
362
- color: white;
414
+ background: var(--light-color-ml);
415
+ color: var(--dark-color-ml);
416
+ position: sticky;
417
+ top: 0;
418
+ z-index: 10;
363
419
  }
364
420
 
365
421
  .table th,
366
422
  .table td {
367
- padding: 0.5rem 0.75rem;
423
+ padding: 0.5rem 1.25rem;
368
424
  vertical-align: middle;
369
425
  border-bottom: 1px solid var(--gray-light-color-ml);
370
- font-size: 0.9rem;
426
+ font-size: 0.95rem;
371
427
  }
372
428
 
373
429
  .table th {
374
- font-weight: 600;
375
- text-transform: uppercase;
376
- font-size: 0.75rem;
377
- letter-spacing: 0.05em;
430
+ font-weight: 700;
431
+ text-transform: none;
432
+ font-size: 0.85rem;
433
+ letter-spacing: 0.02em;
434
+ color: var(--text-muted-color-ml);
435
+ border-bottom: 2px solid var(--gray-light-color-ml);
378
436
  }
379
437
 
380
438
  .table tbody tr {
381
- transition: all 0.2s ease;
439
+ transition: background-color 0.2s ease, transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);
382
440
  position: relative;
441
+ background-color: white;
383
442
  }
384
443
 
385
444
  .table tbody tr:hover {
386
- background: linear-gradient(90deg, rgba(13, 148, 136, 0.03) 0%, transparent 100%);
445
+ background-color: var(--light-color-ml);
387
446
  }
388
447
 
389
448
  .file-row {
390
449
  border-left: 3px solid transparent;
391
- transition: all 0.2s ease;
392
450
  }
393
451
 
394
452
  .file-row:hover {
395
453
  border-left-color: var(--primary-color-ml);
396
- background: linear-gradient(90deg, rgba(13, 148, 136, 0.05) 0%, transparent 100%);
454
+ background-color: #f0fdfa;
455
+ /* Very light teal */
456
+ z-index: 1;
397
457
  }
398
458
 
399
459
  .folder-row {
400
460
  border-left: 3px solid transparent;
401
- transition: all 0.2s ease;
402
461
  cursor: pointer;
403
462
  }
404
463
 
405
464
  .folder-row:hover {
406
465
  border-left-color: #f59e0b;
407
- background: linear-gradient(90deg, rgba(245, 158, 11, 0.05) 0%, transparent 100%);
466
+ background-color: #fffbeb;
467
+ /* Very light amber */
468
+ z-index: 1;
408
469
  }
409
470
 
410
471
  .folder-row .file-icon {
@@ -414,12 +475,11 @@
414
475
  }
415
476
 
416
477
  .folder-row:hover .file-icon {
417
- transform: scale(1.1);
418
478
  box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3);
419
479
  }
420
480
 
421
481
  .breadcrumb-nav {
422
- padding: 1rem 1.5rem;
482
+ padding: 0.5rem 1.5rem;
423
483
  background: linear-gradient(135deg, var(--light-color-ml) 0%, #f9fafb 100%);
424
484
  border-bottom: 1px solid var(--gray-light-color-ml);
425
485
  display: flex;
@@ -459,8 +519,8 @@
459
519
  }
460
520
 
461
521
  .file-icon {
462
- width: 52px;
463
- height: 52px;
522
+ width: 40px;
523
+ height: 40px;
464
524
  background: linear-gradient(135deg, var(--primary-light-color-ml) 0%, rgba(13, 148, 136, 0.08) 100%);
465
525
  border-radius: 10px;
466
526
  display: flex;
@@ -474,7 +534,6 @@
474
534
  }
475
535
 
476
536
  .file-row:hover .file-icon {
477
- transform: scale(1.1);
478
537
  box-shadow: 0 2px 8px rgba(13, 148, 136, 0.2);
479
538
  }
480
539
 
@@ -609,7 +668,7 @@
609
668
  }
610
669
 
611
670
  .empty-state i {
612
- font-size: 5rem;
671
+ font-size: 1.5rem;
613
672
  margin-bottom: 1.5rem;
614
673
  opacity: 0.3;
615
674
  background: linear-gradient(135deg, var(--primary-color-ml), var(--primary-dark-color-ml));
@@ -618,6 +677,12 @@
618
677
  background-clip: text;
619
678
  }
620
679
 
680
+ .fa-add:before,
681
+ .fa-plus:before {
682
+ color: #fff !important;
683
+ align-items: center;
684
+ }
685
+
621
686
  .empty-state h4 {
622
687
  color: var(--dark-color-ml);
623
688
  margin-bottom: 0.75rem;
@@ -1040,8 +1105,8 @@
1040
1105
  }
1041
1106
 
1042
1107
  .skeleton-row .file-icon {
1043
- width: 44px;
1044
- height: 44px;
1108
+ width: 40px;
1109
+ height: 40px;
1045
1110
  border-radius: 8px;
1046
1111
  }
1047
1112
 
@@ -1195,8 +1260,8 @@
1195
1260
  }
1196
1261
 
1197
1262
  .file-row td:nth-child(2) .file-icon {
1198
- width: 56px;
1199
- height: 56px;
1263
+ width: 40px;
1264
+ height: 40px;
1200
1265
  font-size: 1.25rem;
1201
1266
  border-radius: 10px;
1202
1267
  }
@@ -1298,8 +1363,8 @@
1298
1363
 
1299
1364
  /* Make folder icon and name more prominent on mobile */
1300
1365
  .folder-row .file-icon {
1301
- width: 48px;
1302
- height: 48px;
1366
+ width: 40px;
1367
+ height: 40px;
1303
1368
  font-size: 1.2rem;
1304
1369
  margin-right: 0.75rem;
1305
1370
  }
@@ -1324,8 +1389,8 @@
1324
1389
  }
1325
1390
 
1326
1391
  .file-icon {
1327
- width: 48px;
1328
- height: 48px;
1392
+ width: 40px;
1393
+ height: 40px;
1329
1394
  font-size: 1.2rem;
1330
1395
  margin-right: 0.85rem;
1331
1396
  }
@@ -1355,7 +1420,7 @@
1355
1420
  }
1356
1421
 
1357
1422
  .breadcrumb-nav {
1358
- padding: 0.85rem 1rem;
1423
+ padding: 0.5rem 1rem;
1359
1424
  font-size: 0.85rem;
1360
1425
  }
1361
1426
 
@@ -1379,7 +1444,7 @@
1379
1444
  }
1380
1445
 
1381
1446
  .empty-state i {
1382
- font-size: 4rem;
1447
+ font-size: 1.5rem;
1383
1448
  }
1384
1449
 
1385
1450
  .modal-dialog {
@@ -1410,4 +1475,14 @@
1410
1475
  .file-stats .stats-text p {
1411
1476
  font-size: 0.85rem;
1412
1477
  }
1478
+ }
1479
+
1480
+ .dtm {
1481
+ display: none;
1482
+ }
1483
+
1484
+ @media (max-width: 768px) {
1485
+ .dtm {
1486
+ display: block;
1487
+ }
1413
1488
  }
@@ -10,29 +10,63 @@ let pageFolders = [];
10
10
  const bucketAppName = document.getElementById('bucketAppData')?.dataset.appName || '';
11
11
  // Bucket is configured centrally on the server; client should not select a bucket.
12
12
 
13
- // Show alert message
13
+ // Show alert message (Toast)
14
14
  function showAlert(message, type = 'success') {
15
- const alertId = type === 'success' ? 'successAlert' : 'errorAlert';
16
- const messageId = type === 'success' ? 'successMessage' : 'errorMessage';
17
-
18
- const alertEl = document.getElementById(alertId);
19
- const messageEl = document.getElementById(messageId);
20
-
21
- if (alertEl && messageEl) {
22
- messageEl.textContent = message;
23
- alertEl.style.display = 'flex';
24
-
25
- // Auto dismiss after 5 seconds
26
- setTimeout(() => {
27
- alertEl.style.opacity = '0';
28
- alertEl.style.transform = 'translateY(-10px)';
29
- setTimeout(() => {
30
- alertEl.style.display = 'none';
31
- alertEl.style.opacity = '1';
32
- alertEl.style.transform = 'translateY(0)';
33
- }, 300);
34
- }, 5000);
15
+ const container = document.getElementById('toast-container');
16
+ if (!container) {
17
+ // Fallback to old method if container missing
18
+ const alertId = type === 'success' ? 'successAlert' : 'errorAlert';
19
+ const messageId = type === 'success' ? 'successMessage' : 'errorMessage';
20
+ const alertEl = document.getElementById(alertId);
21
+ const messageEl = document.getElementById(messageId);
22
+ if (alertEl && messageEl) {
23
+ messageEl.textContent = message;
24
+ alertEl.style.display = 'flex';
25
+ setTimeout(() => alertEl.style.display = 'none', 5000);
26
+ }
27
+ return;
35
28
  }
29
+
30
+ // Create toast element
31
+ const toast = document.createElement('div');
32
+ const isSuccess = type === 'success';
33
+ toast.className = `toast-notification ${type}`;
34
+ toast.style.cssText = `
35
+ background: ${isSuccess ? 'linear-gradient(135deg, #10b981, #059669)' : 'linear-gradient(135deg, #ef4444, #dc2626)'};
36
+ color: white;
37
+ padding: 1rem 1.5rem;
38
+ border-radius: 8px;
39
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
40
+ display: flex;
41
+ align-items: center;
42
+ gap: 12px;
43
+ min-width: 300px;
44
+ transform: translateX(100%);
45
+ opacity: 0;
46
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
47
+ font-weight: 500;
48
+ `;
49
+
50
+ toast.innerHTML = `
51
+ <i class="fas ${isSuccess ? 'fa-check-circle' : 'fa-exclamation-circle'}" style="font-size: 1.25rem;"></i>
52
+ <div style="flex: 1;">${message}</div>
53
+ <button onclick="this.parentElement.remove()" style="background:none; border:none; color:white; cursor:pointer; opacity:0.8; font-size:1.2rem;">&times;</button>
54
+ `;
55
+
56
+ container.appendChild(toast);
57
+
58
+ // Animate in
59
+ requestAnimationFrame(() => {
60
+ toast.style.transform = 'translateX(0)';
61
+ toast.style.opacity = '1';
62
+ });
63
+
64
+ // Auto dismiss
65
+ setTimeout(() => {
66
+ toast.style.transform = 'translateX(100%)';
67
+ toast.style.opacity = '0';
68
+ setTimeout(() => toast.remove(), 300);
69
+ }, 5000);
36
70
  }
37
71
 
38
72
  // Fetch and render files
@@ -106,12 +140,12 @@ async function loadFiles(page = 1, prefix = '', search = '', sort = currentSort,
106
140
  totalFilesCount.textContent = `${data.totalFiles} ${search ? 'Results' : 'Files'}`;
107
141
 
108
142
  if (search) {
109
- filterInfo.textContent = `Search results for: "${search}"`;
110
- fileListTitle.innerHTML = `Search Results <span class="text-muted">for: </span><span class="badge bg-info">${search}</span>`;
143
+ filterInfo.textContent = `Search results for: "${search}"`;
144
+ fileListTitle.innerHTML = `Search Results <span class="text-muted">for: </span><span class="badge bg-info">${search}</span>`;
111
145
  } else {
112
- const label = prefix || bucketAppName || 'Root';
113
- filterInfo.textContent = `Filtered by folder: ${label}`;
114
- fileListTitle.innerHTML = `Files <span class="text-muted">in: </span><span class="badge bg-info">${label}</span>`;
146
+ const label = prefix || bucketAppName || 'Root';
147
+ filterInfo.textContent = `Filtered by folder: ${label}`;
148
+ fileListTitle.innerHTML = `Files <span class="text-muted">in: </span><span class="badge bg-info">${label}</span>`;
115
149
  }
116
150
 
117
151
  // Render files or empty state
@@ -256,19 +290,62 @@ function changeViewMode(mode) {
256
290
  renderFilesTable(pageFiles, pageFolders, currentPrefix);
257
291
  }
258
292
 
293
+ // Extract folders that match a search query from file paths
294
+ function extractMatchingFolders(files, search) {
295
+ if (!search) return [];
296
+
297
+ const searchLower = search.toLowerCase();
298
+ const folderSet = new Set();
299
+
300
+ files.forEach(file => {
301
+ const parts = file.Key.split('/');
302
+ // Remove filename
303
+ parts.pop();
304
+
305
+ let currentPath = '';
306
+ parts.forEach(part => {
307
+ const segmentPath = currentPath ? `${currentPath}/${part}` : part;
308
+ currentPath = segmentPath;
309
+
310
+ // If any part of the path matches the search, include that full path as a folder
311
+ // AND ensure we don't include the full file path, just the directory
312
+ if (part.toLowerCase().includes(searchLower)) {
313
+ folderSet.add(currentPath);
314
+ }
315
+ });
316
+ });
317
+
318
+ return Array.from(folderSet).sort();
319
+ }
320
+
259
321
  // Render files table
260
322
  function renderFilesTable(files, folders = [], prefix = '') {
261
323
  // Determine which files/folders to show based on view mode
262
324
  let displayFiles = [];
263
325
  let displayFolders = [];
264
326
 
265
- if (currentViewMode === 'flat') {
327
+ // Force flat view if searching
328
+ const isSearchActive = !!currentSearch;
329
+ const mode = isSearchActive ? 'flat' : currentViewMode;
330
+
331
+ if (mode === 'flat') {
266
332
  // In flat mode, show all files in the current loaded page/list regardless of folder depth
267
- // And ignore folder objects (we just show file paths)
268
- displayFiles = files;
269
- // We don't show expandable folders in flat view, unless we want to keep them?
270
- // User asked for "all files displayed without folder". So empty folders.
271
- displayFolders = [];
333
+ if (isSearchActive) {
334
+ // Search Mode:
335
+ // 1. Show files where FILENAME matches the search term
336
+ const searchLower = currentSearch.toLowerCase();
337
+ displayFiles = files.filter(f => {
338
+ const fileName = f.Key.split('/').pop();
339
+ return fileName.toLowerCase().includes(searchLower);
340
+ });
341
+
342
+ // 2. Show folders where FOLDER NAME matches the search term
343
+ displayFolders = extractMatchingFolders(files, currentSearch);
344
+ } else {
345
+ // Normal Flat Mode: Show all files, no folders
346
+ displayFiles = files;
347
+ displayFolders = [];
348
+ }
272
349
  } else {
273
350
  // Normal Folder View logic
274
351
  // Filter files to show only those at current level (not in subfolders)
@@ -338,7 +415,10 @@ function renderFilesTable(files, folders = [], prefix = '') {
338
415
  <div class="file-name" style="font-weight: 700;">
339
416
  <i class="fas fa-folder" style="margin-right: 0.5rem; color: #f59e0b;"></i>${folderName}/
340
417
  </div>
341
- </div>
418
+ </div>
419
+ <button type="button" class="btn btn-danger btn-sm delete-btn dtm" onclick="event.stopPropagation(); confirmDelete('${folderPath}/', true);" data-key="${folderPath}/" data-is-folder="true" title="Delete folder">
420
+ <i class="fas fa-trash"></i>
421
+ </button>
342
422
  </div>
343
423
  </td>
344
424
  <td>
@@ -349,7 +429,7 @@ function renderFilesTable(files, folders = [], prefix = '') {
349
429
  </td>
350
430
  <td style="text-align: center;">
351
431
  <div class="btn-group" role="group">
352
- <button type="button" class="btn btn-danger btn-sm delete-btn" data-key="${folderPath}/" data-is-folder="true" title="Delete folder">
432
+ <button type="button" class="btn btn-danger btn-sm delete-btn" onclick="event.stopPropagation(); confirmDelete('${folderPath}/', true);" data-key="${folderPath}/" data-is-folder="true" title="Delete folder">
353
433
  <i class="fas fa-trash"></i>
354
434
  </button>
355
435
  </div>
@@ -431,18 +511,23 @@ function renderFilesTable(files, folders = [], prefix = '') {
431
511
  if (allRows === '') {
432
512
  document.getElementById('fileListContainer').innerHTML = `
433
513
  <div class="empty-state">
434
- <i class="fas fa-folder-open"></i>
435
- <h4>No files or folders</h4>
436
- <p>${prefix ? `No files in folder: ${prefix}` : 'This folder is empty'}</p>
437
- <div style="display:flex; gap:0.5rem; justify-content:center; margin-top:1rem;">
438
- <button class="btn btn-primary" onclick="document.getElementById('fileInput').click()">
439
- <i class="fas fa-cloud-upload-alt"></i> Upload File
514
+ <div style="margin-bottom: 1.5rem; position: relative; display: inline-block;">
515
+ <i class="fas fa-folder-open" style="font-size: 4rem; color: #cbd5e0;"></i>
516
+ <i class="fas fa-search" style="font-size: 2rem; color: var(--primary-color-ml); position: absolute; bottom: -5px; right: -10px; background: white; border-radius: 50%; padding: 5px;"></i>
517
+ </div>
518
+ <h4>No files found</h4>
519
+ <p style="max-width: 400px; margin: 0 auto 1.5rem; color: var(--text-muted-color-ml);">
520
+ ${prefix ? `This folder <strong>${prefix}</strong> is currently empty.` : 'Your bucket is empty. Start by uploading files or creating a new folder.'}
521
+ </p>
522
+ <div style="display:flex; gap:0.75rem; justify-content:center; flex-wrap: wrap;">
523
+ <button class="btn btn-primary" onclick="window.scrollTo({top: 0, behavior: 'smooth'}); document.getElementById('fileInput').click()">
524
+ <i class="fas fa-cloud-upload-alt"></i> Upload Files
440
525
  </button>
441
526
  <button class="btn btn-outline-secondary" onclick="createFolder()">
442
- <i class="fas fa-folder-plus"></i> Create Folder
527
+ <i class="fas fa-folder-plus"></i> New Folder
443
528
  </button>
444
529
  ${prefix ? `<button class="btn btn-light" onclick="loadFiles(1, '')">
445
- <i class="fas fa-arrow-left"></i> Back to Root
530
+ <i class="fas fa-home"></i> Go to Root
446
531
  </button>` : ''}
447
532
  </div>
448
533
  </div>
@@ -461,7 +546,7 @@ function renderFilesTable(files, folders = [], prefix = '') {
461
546
  document.getElementById('fileListContainer').innerHTML = `
462
547
  ${breadcrumb}
463
548
  <div class="table-responsive">
464
- <div id="bulkActionsBar" style="display:none; padding: 0.75rem; border-bottom: 1px solid var(--gray-light-color-ml); background: var(--light-color-ml);">
549
+ <div id="bulkActionsBar" style="display:none; padding: 0.5rem; border-bottom: 1px solid var(--gray-light-color-ml); background: var(--light-color-ml);">
465
550
  <span id="bulkSelectedCount">0 selected</span>
466
551
  <div style="float: right;">
467
552
  <button class="btn btn-danger btn-sm" id="bulkDeleteBtn" disabled>
@@ -665,10 +750,10 @@ function renderUploadQueue() {
665
750
  container.innerHTML = uploadQueue.map(item => `
666
751
  <div class="upload-item" data-id="${item.id}">
667
752
  <div class="meta">
668
- <div class="file-icon"><i class="fas fa-file"></i></div>
669
- <div>
753
+ <div style="display:flex; align-items:center; gap:0.75rem; min-width:0; width:100%; margin-bottom:5px;">
754
+ <div class="file-icon"><i class="fas fa-file"></i></div>
670
755
  <div class="name">${item.file.name}</div>
671
- <div class="form-text">${formatFileSize(item.file.size)}</div>
756
+ <div class="form-text file-size" style="white-space:nowrap; color:var(--text-muted-color-ml); font-size:0.875rem; margin-left:auto;">${formatFileSize(item.file.size)}</div>
672
757
  </div>
673
758
  </div>
674
759
  <div style="min-width:220px;">
@@ -828,22 +913,7 @@ function startAllUploads() {
828
913
  document.getElementById('startUploadBtn')?.addEventListener('click', startAllUploads);
829
914
  document.getElementById('clearQueueBtn')?.addEventListener('click', function () { uploadQueue.length = 0; renderUploadQueue(); });
830
915
 
831
- // Reset upload form
832
- function resetUploadForm() {
833
- const uploadBtn = document.getElementById('uploadBtn');
834
- const uploadProgress = document.getElementById('uploadProgress');
835
916
 
836
- setTimeout(() => {
837
- uploadBtn.disabled = false;
838
- uploadBtn.classList.remove('uploading');
839
- uploadBtn.innerHTML = '<i class="fas fa-cloud-upload-alt"></i> Upload File';
840
- uploadProgress.style.display = 'none';
841
-
842
- // Reset progress
843
- document.getElementById('progressBar').style.width = '0%';
844
- document.getElementById('uploadPercent').textContent = '0%';
845
- }, 3000);
846
- }
847
917
 
848
918
  // Add hover effects and animations
849
919
  document.addEventListener('DOMContentLoaded', function () {
@@ -853,10 +923,16 @@ document.addEventListener('DOMContentLoaded', function () {
853
923
  const initialPage = parseInt(urlParams.get('page')) || 1;
854
924
  loadFiles(initialPage, initialPrefix);
855
925
 
856
- // Debounced search input (improves UX and reduces API calls)
926
+ // Search behavior: Trigger when the Search button is clicked OR when the user presses Enter in the search input
857
927
  const searchInputEl = document.getElementById('searchInput');
858
- if (searchInputEl && typeof debounce === 'function') {
859
- searchInputEl.addEventListener('input', debounce(() => performSearch(), 350));
928
+ if (searchInputEl) {
929
+ // Trigger search when Enter is pressed in the input
930
+ searchInputEl.addEventListener('keydown', function (e) {
931
+ if (e.key === 'Enter') {
932
+ e.preventDefault();
933
+ performSearch();
934
+ }
935
+ });
860
936
  }
861
937
 
862
938
  // Close modal when clicking outside
@@ -997,6 +1073,7 @@ function showPreviewModal(encodedKey, filename, extension) {
997
1073
  contentEl.appendChild(iframe);
998
1074
  } else if (textTypes.includes(extension)) {
999
1075
  dialog.classList.add('preview-compact');
1076
+
1000
1077
  fetch(`/mbkbucket/view/${encodedKey}`)
1001
1078
  .then(r => r.text())
1002
1079
  .then(text => {
@@ -1223,9 +1300,47 @@ async function createFolder() {
1223
1300
  }
1224
1301
  }
1225
1302
 
1226
- // Start/clear upload buttons
1227
- document.getElementById('startUploadBtn')?.addEventListener('click', startAllUploads);
1228
- document.getElementById('clearQueueBtn')?.addEventListener('click', function () { uploadQueue.length = 0; renderUploadQueue(); });
1303
+ // Create new file
1304
+ async function createFile() {
1305
+ let name = prompt('Enter new file name:');
1306
+ if (!name) return;
1307
+
1308
+ name = name.trim();
1309
+ if (!name.includes('.')) {
1310
+ name += '.txt';
1311
+ }
1312
+
1313
+ // Ensure no leading slashes, we upload to currentPrefix
1314
+ if (name.startsWith('/')) name = name.substring(1);
1315
+
1316
+ // Check if valid
1317
+ if (!name) return showAlert('Invalid file name', 'error');
1318
+
1319
+ // Create empty file content
1320
+ const file = new File([" "], name, { type: "text/plain" });
1321
+ const formData = new FormData();
1322
+ formData.append('file', file);
1323
+ formData.append('prefix', currentPrefix);
1324
+
1325
+ try {
1326
+ const resp = await fetch('/mbkbucket/upload', {
1327
+ method: 'POST',
1328
+ body: formData
1329
+ });
1330
+
1331
+ const json = await resp.json();
1332
+ if (json.success) {
1333
+ showAlert('File created', 'success');
1334
+ loadFiles(currentPage, currentPrefix);
1335
+ } else {
1336
+ showAlert(json.error || 'Create file failed', 'error');
1337
+ }
1338
+ } catch (err) {
1339
+ showAlert('Create file failed: ' + err.message, 'error');
1340
+ }
1341
+ }
1342
+
1343
+
1229
1344
 
1230
1345
  // mbkbucket shared utilities (exposed to global window for backward compatibility)
1231
1346
  (function (w) {
@@ -125,7 +125,7 @@
125
125
  display: flex;
126
126
  align-items: center;
127
127
  justify-content: space-between;
128
- margin-bottom: 2rem;
128
+ margin-bottom: 1rem;
129
129
  padding-bottom: 1rem;
130
130
  border-bottom: 2px solid var(--gray-light-color-ml)
131
131
  }
@@ -1,6 +1,6 @@
1
1
  {{!-- File Management System - Bucket Dashboard --}}
2
- <link rel="stylesheet" href="/css/bucketadmin.css?v={{assetVersion}}">
3
- <div id="bucketAppData" data-app-name="{{mbkautheVar.APP_NAME}}" hidden></div>
2
+ <link rel="stylesheet" href="/mbkbucket/bucketadmin.css?v={{assetVersion}}">
3
+ <div id="bucketAppData" data-app-name="{{APP_NAME}}" hidden></div>
4
4
  <!-- File Statistics -->
5
5
  <div class="dashboard-card file-stats">
6
6
  <div class="card-body">
@@ -36,9 +36,9 @@
36
36
  </div>
37
37
  {{/if}}
38
38
 
39
- <div class="row g-4 mb-4">
39
+ <div class="mb-4">
40
40
  <!-- Upload Form -->
41
- <div class="col-lg-6">
41
+ <div class="col-lg-12">
42
42
  <div class="dashboard-card h-100">
43
43
  <div class="card-header">
44
44
  <h5 class="card-title">
@@ -54,14 +54,14 @@
54
54
 
55
55
  <div class="mb-3">
56
56
  <label for="fileInput" class="form-label">Select Files or drag & drop</label>
57
- <div id="dropZone" class="form-control form-control-lg"
58
- style="min-height:120px; display:flex; align-items:center; justify-content:center; cursor:pointer; text-align:center;">
59
- <div>
60
- <i class="fas fa-cloud-upload-alt fa-2x" style="opacity: .65;"></i>
61
- <div style="margin-top: 0.5rem; color: var(--gray-color-ml);">Drop files here or click to browse</div>
57
+ <div id="dropZone" class="upload-drop-zone">
58
+ <div class="drop-zone-content">
59
+ <i class="fas fa-cloud-upload-alt"></i>
60
+ <div class="drop-text">Drop files here or click to browse</div>
61
+ <div class="drop-hint">Supports multiple files</div>
62
62
  </div>
63
63
  </div>
64
- <input type="file" class="form-control d-none" id="fileInput" name="files" multiple>
64
+ <input type="file" class="form-control d-none" style="display: none;" id="fileInput" name="files" multiple>
65
65
  <div class="form-text">
66
66
  <i class="fas fa-info-circle"></i>
67
67
  You can drop multiple files. Large files will use chunked upload when available.
@@ -99,41 +99,6 @@
99
99
  </div>
100
100
  </div>
101
101
  </div>
102
-
103
- <!-- Search Filter -->
104
- <div class="col-lg-6">
105
- <div class="dashboard-card h-100">
106
- <div class="card-header">
107
- <h5 class="card-title">
108
- <div class="card-icon">
109
- <i class="fas fa-search"></i>
110
- </div>
111
- Search Files & Folders
112
- </h5>
113
- </div>
114
- <div class="card-body">
115
- <div class="mb-3">
116
- <label for="searchInput" class="form-label">Search by Name</label>
117
- <input type="text" class="form-control form-control-lg" id="searchInput" placeholder="Enter search term..."
118
- onkeypress="if(event.key==='Enter') performSearch()">
119
- <div class="form-text">
120
- <i class="fas fa-info-circle"></i>
121
- Search across all files and folders by name (case-insensitive)
122
- </div>
123
- </div>
124
- <div class="d-grid gap-2 d-md-flex justify-content-md-end">
125
- <button type="button" class="btn btn-primary btn-lg" onclick="performSearch()">
126
- <i class="fas fa-search"></i>
127
- Search
128
- </button>
129
- <button type="button" class="btn btn-outline-secondary btn-lg" onclick="clearSearch()">
130
- <i class="fas fa-times"></i>
131
- Clear
132
- </button>
133
- </div>
134
- </div>
135
- </div>
136
- </div>
137
102
  </div>
138
103
 
139
104
  <!-- File List -->
@@ -145,7 +110,23 @@
145
110
  </div>
146
111
  <span id="fileListTitle">Files</span>
147
112
  </h5>
148
-
113
+
114
+ <!-- Inline Search (moved from separate card) -->
115
+ <div class="search-inline" style="flex:1; max-width:640px; display:flex; gap:0.5rem; align-items:center; justify-content:center;">
116
+ <input type="text" class="form-control form-control-sm" id="searchInput" placeholder="Search files and folders..." aria-label="Search files">
117
+ <button type="button" class="btn btn-sm btn-primary" onclick="performSearch()" aria-label="Search"><i class="fas fa-search"></i></button>
118
+ <button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearSearch()" aria-label="Clear search"><i class="fas fa-times"></i></button>
119
+ </div>
120
+
121
+ <div class="btn-group" role="group" aria-label="Actions">
122
+ <button type="button" class="btn btn-sm btn-success" onclick="createFile()" title="New Text File">
123
+ <i class="fas fa-file-alt"></i> New File
124
+ </button>
125
+ <button type="button" class="btn btn-sm btn-outline-success" onclick="createFolder()" title="New Folder">
126
+ <i class="fas fa-folder-plus"></i> New Folder
127
+ </button>
128
+ </div>
129
+
149
130
  <div class="btn-group" role="group" aria-label="View Mode">
150
131
  <button type="button" class="btn btn-sm btn-light view-toggle-btn active" id="viewBtnFolder" onclick="changeViewMode('folder')" title="Folder View" style="border-top-right-radius: 0; border-bottom-right-radius: 0;">
151
132
  <i class="fas fa-folder"></i> Folders
@@ -230,7 +211,7 @@
230
211
  </div>
231
212
  </div>
232
213
  <div class="modal-body">
233
- <div class="preview-content" aria-live="polite">
214
+ <div class="preview-content" id="previewContent" aria-live="polite">
234
215
  <div class="empty-preview">
235
216
  <i class="fas fa-spinner fa-spin"></i>
236
217
  <div style="margin-top:0.5rem; color:var(--gray-color-ml);">Loading preview...</div>
@@ -250,4 +231,7 @@
250
231
  </div>
251
232
  </div>
252
233
 
253
- <script src="/js/bucketadmincontent.js?v={{assetVersion}}"></script>
234
+ <!-- Toast Container -->
235
+ <div id="toast-container" style="position: fixed; bottom: 20px; right: 20px; z-index: 1050; display: flex; flex-direction: column; gap: 10px;"></div>
236
+
237
+ <script src="/mbkbucket/bucketadmin.js?v={{assetVersion}}"></script>