mbkbucket 3.0.0 → 3.1.0

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.
@@ -34,7 +34,9 @@ function resolvePrefix(req, res, prefix) {
34
34
  * Build a full S3 key from a prefix and filename, stripping trailing slashes.
35
35
  */
36
36
  function buildKey(prefix, fileName) {
37
- return `${prefix.replace(/\/+$/, '')}/${fileName}`;
37
+ const cleanPrefix = String(prefix || '').replace(/^\/+/, '').replace(/\/+$/, '');
38
+ const cleanName = String(fileName || '').replace(/^\/+/, '');
39
+ return cleanPrefix ? `${cleanPrefix}/${cleanName}` : cleanName;
38
40
  }
39
41
 
40
42
  async function cleanupFolderMarker(folderPath, bucketName) {
@@ -121,8 +123,8 @@ export async function listFiles(req, res) {
121
123
  let legacyFallback = false;
122
124
 
123
125
  if (useOptimizedListing) {
124
- let listPrefix = effectivePrefix;
125
- if (listPrefix === getAppName() && !listPrefix.endsWith('/')) {
126
+ let listPrefix = effectivePrefix ? String(effectivePrefix).replace(/^\/+/, '') : '';
127
+ if (listPrefix && !listPrefix.endsWith('/')) {
126
128
  listPrefix += '/';
127
129
  }
128
130
 
@@ -135,11 +137,12 @@ export async function listFiles(req, res) {
135
137
 
136
138
  files = (result.Contents || []).filter(f => !f.Key.endsWith('/'));
137
139
  folders = (result.CommonPrefixes || [])
138
- .filter(p => p.Prefix !== effectivePrefix)
140
+ .filter(p => p.Prefix !== listPrefix && p.Prefix !== effectivePrefix)
139
141
  .map(p => p.Prefix);
140
142
  nextContinuationToken = result.NextContinuationToken;
143
+ }
141
144
 
142
- } else {
145
+ else {
143
146
  let continuationToken = undefined;
144
147
 
145
148
  do {
@@ -350,7 +353,14 @@ export async function createFolder(req, res) {
350
353
  const effectivePrefix = resolvePrefix(req, res, prefix);
351
354
  if (effectivePrefix === null) return;
352
355
 
353
- const folderKey = `${effectivePrefix.replace(/\/+$/, '')}/${folderName}/`;
356
+ const cleanName = String(folderName).replace(/^\/+/, '').replace(/\/+$/, '');
357
+ let folderKey;
358
+ if (effectivePrefix && !cleanName.startsWith(effectivePrefix)) {
359
+ const cleanPrefix = effectivePrefix.replace(/\/+$/, '');
360
+ folderKey = `${cleanPrefix}/${cleanName}/`;
361
+ } else {
362
+ folderKey = `${cleanName}/`;
363
+ }
354
364
 
355
365
  const markerExists = await fileExists(folderKey, bucketName);
356
366
  if (markerExists) return res.status(409).json({ success: false, error: 'Folder already exists' });
@@ -7,7 +7,7 @@
7
7
  <title>{{#if page}}{{page}} - {{/if}}Web Portal</title>
8
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
- <link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700;800&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
10
+ <link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700;800&family=IBM+Plex+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
11
11
  <script src="/mbkauthe/main.js" defer></script>
12
12
  <script src="https://portal.mbktech.org/Assets/Scripts/auto-ids.js" defer></script>
13
13
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
@@ -26,26 +26,34 @@
26
26
  --warning-color-ml: #d97706;
27
27
  --success-color-ml: #16a34a;
28
28
  --info-color-ml: #3b82f6;
29
- --border-radius-r-ml: 0.5rem;
30
- --border-radius-sm-ml: 0.25rem;
31
- --border-radius-lg-ml: 0.75rem;
32
- --shadow-s-ml: 0 2px 4px rgba(0, 0, 0, 0.1);
33
- --shadow-md-ml: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
34
- --shadow-lg-ml: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
35
- --transition-base-ml: all 0.2s ease;
36
- --transition-fast-ml: all 0.15s ease;
37
- --main-border-color: var(--gray-light-color-ml);
38
- --color-accent: var(--primary-color-ml);
39
- --color-text: var(--dark-color-ml);
40
- --color-bg: var(--light-color-ml);
29
+
30
+ --primary: var(--primary-color-ml);
31
+ --primary-lt: var(--primary-light-color-ml);
32
+ --primary-dk: var(--primary-dark-color-ml);
33
+ --primary-hover: #0d635c;
34
+
35
+ --body-bg: var(--light-color-ml);
36
+ --bg-surface: rgba(255, 255, 255, 0.88);
37
+ --bg-surface-secondary: rgba(244, 247, 243, 0.7);
38
+ --bg-surface-tertiary: #e9f2ee;
39
+
40
+ --border-color: rgba(15, 118, 110, 0.15);
41
+ --border-color-light: rgba(15, 118, 110, 0.08);
42
+
43
+ --text: var(--dark-color-ml);
44
+ --text-secondary: #4a5c5a;
45
+ --text-muted: var(--gray-color-ml);
46
+ --text-disabled: #a0b2af;
47
+
41
48
  --font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
42
- --font-family-headings: "Sora", "Segoe UI", sans-serif
49
+ --font-family-headings: "Sora", "Segoe UI", sans-serif;
50
+ --font-mono: 'JetBrains Mono', monospace;
43
51
  }
44
52
 
45
53
  * {
46
54
  margin: 0;
47
55
  padding: 0;
48
- box-sizing: border-box
56
+ box-sizing: border-box;
49
57
  }
50
58
 
51
59
  body {
@@ -61,7 +69,7 @@
61
69
  line-height: 1.6;
62
70
  font-size: 14px;
63
71
  position: relative;
64
- isolation: isolate
72
+ isolation: isolate;
65
73
  }
66
74
 
67
75
  body::before,
@@ -70,7 +78,7 @@
70
78
  position: fixed;
71
79
  border-radius: 999px;
72
80
  pointer-events: none;
73
- z-index: -1
81
+ z-index: -1;
74
82
  }
75
83
 
76
84
  body::before {
@@ -78,7 +86,7 @@
78
86
  height: 260px;
79
87
  left: -80px;
80
88
  top: 18vh;
81
- background: radial-gradient(circle, rgba(15, 118, 110, 0.16) 0, rgba(15, 118, 110, 0) 70%)
89
+ background: radial-gradient(circle, rgba(15, 118, 110, 0.16) 0, rgba(15, 118, 110, 0) 70%);
82
90
  }
83
91
 
84
92
  body::after {
@@ -86,23 +94,7 @@
86
94
  height: 300px;
87
95
  right: -120px;
88
96
  bottom: 10vh;
89
- background: radial-gradient(circle, rgba(251, 146, 60, 0.18) 0, rgba(251, 146, 60, 0) 72%)
90
- }
91
-
92
- /* Removed unused sidebar/menu styles to reduce CSS size. */
93
-
94
- .logo {
95
- font-family: var(--font-family-headings);
96
- font-size: 1.65rem;
97
- font-weight: 700;
98
- color: #0b4f4a;
99
- display: flex;
100
- align-items: center;
101
- gap: .5rem
102
- }
103
-
104
- .logo img {
105
- width: 35px
97
+ background: radial-gradient(circle, rgba(251, 146, 60, 0.18) 0, rgba(251, 146, 60, 0) 72%);
106
98
  }
107
99
 
108
100
  .main-content {
@@ -110,7 +102,7 @@
110
102
  display: flex;
111
103
  flex-direction: column;
112
104
  overflow: hidden;
113
- transition: var(--transition-base-ml)
105
+ min-height: 100vh;
114
106
  }
115
107
 
116
108
  .top-nav {
@@ -119,139 +111,77 @@
119
111
  padding: .75rem 1.5rem;
120
112
  display: flex;
121
113
  align-items: center;
122
- justify-content: flex-end;
123
- box-shadow: var(--shadow-md-ml);
114
+ justify-content: space-between;
115
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.06), 0 2px 4px -1px rgba(0, 0, 0, 0.04);
124
116
  z-index: 90;
125
117
  border-bottom: 1px solid rgba(255, 255, 255, .75);
126
- gap: 1rem
118
+ gap: 1rem;
127
119
  }
128
120
 
129
- /* Removed unused notification and profile styles to reduce CSS size. */
121
+ .logo {
122
+ font-family: var(--font-family-headings);
123
+ font-size: 1.5rem;
124
+ font-weight: 700;
125
+ color: #0b4f4a;
126
+ display: flex;
127
+ align-items: center;
128
+ gap: .5rem;
129
+ cursor: pointer;
130
+ text-decoration: none;
131
+ }
132
+
133
+ .logo img {
134
+ width: 32px;
135
+ height: 32px;
136
+ }
130
137
 
131
138
  .content-area {
132
139
  flex: 1;
133
- padding: 1.6rem;
134
- overflow-y: auto
140
+ padding: 1.5rem;
141
+ overflow-y: auto;
135
142
  }
136
143
 
137
144
  .content-area::-webkit-scrollbar {
138
- width: 8px
145
+ width: 8px;
139
146
  }
140
147
 
141
148
  .content-area::-webkit-scrollbar-track {
142
- background: 0 0
149
+ background: transparent;
143
150
  }
144
151
 
145
152
  .content-area::-webkit-scrollbar-thumb {
146
153
  background: var(--gray-color-ml);
147
- border-radius: 4px
154
+ border-radius: 4px;
148
155
  }
149
156
 
150
157
  .content-area::-webkit-scrollbar-thumb:hover {
151
- background: var(--primary-color-ml)
158
+ background: var(--primary-color-ml);
152
159
  }
153
160
 
154
- .page-header {
155
- display: flex;
156
- align-items: center;
157
- justify-content: space-between;
158
- margin-bottom: 1.2rem;
159
- padding: 1rem 1.2rem;
160
- border: 1px solid rgba(255, 255, 255, .9);
161
- border-radius: 1rem;
162
- background: rgba(255, 255, 255, .74);
163
- box-shadow: 0 12px 34px rgba(15, 23, 42, .07)
164
- }
165
-
166
- .page-title {
167
- font-family: var(--font-family-headings);
168
- font-size: 1.5rem;
169
- font-weight: 700;
170
- color: var(--dark-color-ml);
171
- margin: 0
172
- }
173
-
174
- .breadcrumb {
175
- display: flex;
176
- align-items: center;
177
- color: var(--gray-color-ml);
178
- font-size: .875rem;
179
- background: linear-gradient(135deg, rgba(15, 118, 110, .08), rgba(251, 146, 60, .08));
180
- padding: .4rem .85rem;
181
- border-radius: 999px;
182
- border: 1px solid rgba(15, 118, 110, .15)
183
- }
184
-
185
- .breadcrumb a {
186
- color: var(--primary-color-ml);
187
- text-decoration: none;
188
- transition: var(--transition-base-ml);
189
- font-weight: 500
190
- }
191
-
192
- .breadcrumb a:hover {
193
- color: var(--primary-dark-color-ml)
194
- }
195
-
196
- .breadcrumb .separator {
197
- margin: 0 .5rem;
198
- color: var(--gray-color-ml)
199
- }
200
-
201
- @media (max-width:992px) {
202
- .main-content {
203
- margin-left: 0 !important
204
- }
205
-
206
- .content-area {
207
- padding: 1.5rem
208
- }
209
- }
210
-
211
- @media (max-width:768px) {
161
+ @media (max-width: 768px) {
212
162
  .top-nav {
213
- padding: .25rem 1rem
163
+ padding: .5rem 1rem;
214
164
  }
215
-
216
165
  .content-area {
217
- padding: 1rem
218
- }
219
-
220
- .page-header {
221
- flex-direction: row;
222
- align-items: flex-start;
223
- gap: 1rem
224
- }
225
-
226
- .page-title {
227
- font-size: 1.5rem
166
+ padding: 0.85rem;
228
167
  }
229
168
  }
230
-
231
-
232
169
  </style>
233
170
  {{> showmessage }}
234
171
  </head>
235
172
 
236
173
  <body>
237
-
238
174
  <div class="main-content">
239
175
  <nav class="top-nav">
240
- <div class="logo">
241
- <img onclick="window.location='/mbkbucket'" src="/icon.svg" alt="Logo">
242
- <span onclick="window.location='/mbkbucket'" style="cursor:pointer;">BKBucket</span>
176
+ <div class="logo" onclick="window.location='/mbkbucket'">
177
+ <img src="/icon.svg" alt="Logo">
178
+ <span>MBKBucket</span>
243
179
  </div>
244
180
 
245
181
  {{> profilemenu }}
246
182
  </nav>
247
183
 
248
184
  <div class="content-area">
249
- <div class="page-header">
250
- <h1 class="page-title">{{#if page}}{{page}}{{else}}Dashboard{{/if}}</h1>
251
- <div class="breadcrumb">
252
- <span>{{#if page}}{{page}}{{else}}Home{{/if}}</span>
253
- </div>
254
- </div>
255
185
  {{> bucketadmincontent }}
256
186
  </div>
257
187
  </div>
@@ -1,37 +1,33 @@
1
1
  <!-- Delete Confirmation Modal -->
2
2
  <div class="modal" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
3
- <div class="modal-dialog">
3
+ <div class="modal-dialog modal-dialog-centered">
4
4
  <div class="modal-content">
5
5
  <div class="modal-header">
6
6
  <h5 class="modal-title" id="deleteModalLabel">
7
- <i class="fas fa-exclamation-triangle"></i>
7
+ <svg width="18" height="18" viewBox="0 0 16 16" fill="none" stroke="var(--red)" stroke-width="1.5" class="modal-title-icon"><path d="M3 4h10M6 4V2h4v2M5 4l1 9h4l1-9"/></svg>
8
8
  Confirm Delete
9
9
  </h5>
10
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"
11
- onclick="closeModal()">&times;</button>
10
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="closeModal()">&times;</button>
12
11
  </div>
13
- <div class="modal-body">
14
- <i class="fas fa-trash-alt fa-3x"></i>
15
- <h6>Are you sure you want to delete this file?</h6>
16
- <p class="modal-body-text-style">
17
- <strong id="deleteFileName" style="color: var(--dark-color-ml);"></strong>
18
- </p>
19
- <p class="modal-body-text-top">
20
- <small>This action cannot be undone.</small>
12
+ <div class="modal-body text-center py-4">
13
+ <div class="delete-icon-wrap mb-3">
14
+ <svg width="44" height="44" viewBox="0 0 16 16" fill="none" stroke="var(--red)" stroke-width="1.5"><path d="M3 4h10M6 4V2h4v2M5 4l1 9h4l1-9"/></svg>
15
+ </div>
16
+ <h6 class="modal-confirm-heading">Are you sure you want to delete this?</h6>
17
+ <p class="modal-item-name-preview mt-2">
18
+ <code id="deleteFileName"></code>
21
19
  </p>
20
+ <p class="text-muted small mt-2">This action cannot be undone.</p>
22
21
  </div>
23
22
  <div class="modal-footer">
24
- <button type="button" class="btn btn-secondary" onclick="closeModal()">
25
- <i class="fas fa-times"></i>
26
- Cancel
27
- </button>
23
+ <button type="button" class="btn btn-outline" onclick="closeModal()">Cancel</button>
28
24
  <form id="deleteForm" method="post" action="/mbkbucket/delete" style="display: inline;">
29
25
  <input type="hidden" name="key" id="deleteKey">
30
26
  <input type="hidden" name="prefix" value="{{prefix}}">
31
27
  <input type="hidden" name="folder" id="deleteIsFolder" value="">
32
28
  <button type="submit" class="btn btn-danger" id="deleteConfirmButton">
33
- <i class="fas fa-trash"></i>
34
- <span id="deleteConfirmLabel">Delete File</span>
29
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 4h10M6 4V2h4v2M5 4l1 9h4l1-9"/></svg>
30
+ <span id="deleteConfirmLabel">Delete</span>
35
31
  </button>
36
32
  </form>
37
33
  </div>
@@ -1,20 +1,32 @@
1
- <div class="table-responsive" aria-label="Loading files" aria-busy="true">
2
- <div class="skeleton-row">
3
- <div class="skeleton-box s-40"></div>
4
- <div class="skeleton-box s-200"></div>
5
- <div class="skeleton-box s-80"></div>
6
- <div class="skeleton-box s-200"></div>
1
+ <div class="fm-skeleton-grid" aria-label="Loading files" aria-busy="true">
2
+ <div class="fm-skeleton-card">
3
+ <div class="fm-skeleton-icon"></div>
4
+ <div class="fm-skeleton-line w-75"></div>
5
+ <div class="fm-skeleton-line w-50"></div>
7
6
  </div>
8
- <div class="skeleton-row">
9
- <div class="skeleton-box s-40"></div>
10
- <div class="skeleton-box s-200"></div>
11
- <div class="skeleton-box s-80"></div>
12
- <div class="skeleton-box s-200"></div>
7
+ <div class="fm-skeleton-card">
8
+ <div class="fm-skeleton-icon"></div>
9
+ <div class="fm-skeleton-line w-75"></div>
10
+ <div class="fm-skeleton-line w-50"></div>
13
11
  </div>
14
- <div class="skeleton-row">
15
- <div class="skeleton-box s-40"></div>
16
- <div class="skeleton-box s-200"></div>
17
- <div class="skeleton-box s-80"></div>
18
- <div class="skeleton-box s-200"></div>
12
+ <div class="fm-skeleton-card">
13
+ <div class="fm-skeleton-icon"></div>
14
+ <div class="fm-skeleton-line w-75"></div>
15
+ <div class="fm-skeleton-line w-50"></div>
16
+ </div>
17
+ <div class="fm-skeleton-card">
18
+ <div class="fm-skeleton-icon"></div>
19
+ <div class="fm-skeleton-line w-75"></div>
20
+ <div class="fm-skeleton-line w-50"></div>
21
+ </div>
22
+ <div class="fm-skeleton-card">
23
+ <div class="fm-skeleton-icon"></div>
24
+ <div class="fm-skeleton-line w-75"></div>
25
+ <div class="fm-skeleton-line w-50"></div>
26
+ </div>
27
+ <div class="fm-skeleton-card">
28
+ <div class="fm-skeleton-icon"></div>
29
+ <div class="fm-skeleton-line w-75"></div>
30
+ <div class="fm-skeleton-line w-50"></div>
19
31
  </div>
20
32
  </div>
@@ -0,0 +1,29 @@
1
+ <!-- New Folder Modal -->
2
+ <div class="modal" id="newFolderModal" tabindex="-1" aria-labelledby="newFolderModalLabel" aria-hidden="true">
3
+ <div class="modal-dialog modal-dialog-centered">
4
+ <div class="modal-content">
5
+ <div class="modal-header">
6
+ <h5 class="modal-title" id="newFolderModalLabel">
7
+ <svg width="18" height="18" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="modal-title-icon"><path d="M2 4a1 1 0 011-1h3l2 2h5a1 1 0 011 1v6a1 1 0 01-1 1H3a1 1 0 01-1-1V4z"/><path d="M8 7v4M6 9h4"/></svg>
8
+ New Folder
9
+ </h5>
10
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="closeNewFolderModal()">&times;</button>
11
+ </div>
12
+ <div class="modal-body">
13
+ <form id="newFolderForm" onsubmit="event.preventDefault(); submitCreateFolder();">
14
+ <div class="modal-form-row">
15
+ <label for="newFolderNameInput" class="form-label">Folder Name</label>
16
+ <input type="text" id="newFolderNameInput" class="form-control" placeholder="e.g. documents, images, backup" autocomplete="off" required>
17
+ <div class="form-text">Folders are created in the current directory path.</div>
18
+ </div>
19
+ </form>
20
+ </div>
21
+ <div class="modal-footer">
22
+ <button type="button" class="btn btn-outline" onclick="closeNewFolderModal()">Cancel</button>
23
+ <button type="button" class="btn btn-primary" id="submitNewFolderBtn" onclick="submitCreateFolder()">
24
+ Create Folder
25
+ </button>
26
+ </div>
27
+ </div>
28
+ </div>
29
+ </div>
@@ -1,31 +1,38 @@
1
1
  <!-- Preview Modal -->
2
2
  <div class="modal" id="previewModal" role="dialog" aria-modal="true" aria-labelledby="previewModalLabel" tabindex="-1">
3
- <div class="modal-dialog preview-modal-dialog" role="document">
3
+ <div class="modal-dialog modal-dialog-centered preview-modal-dialog" role="document">
4
4
  <div class="modal-content">
5
5
  <div class="modal-header">
6
- <h5 class="modal-title" id="previewModalLabel"><i class="fas fa-eye"></i> <span
7
- id="previewFileName">Preview</span></h5>
6
+ <h5 class="modal-title" id="previewModalLabel">
7
+ <svg width="18" height="18" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="modal-title-icon"><path d="M1 8s3-5 7-5 7 5 7 5-3 5-7 5-7-5-7-5z"/><circle cx="8" cy="8" r="2.5"/></svg>
8
+ <span id="previewFileName">Preview</span>
9
+ </h5>
8
10
  <div class="preview-header-controls">
9
- <button id="previewFullscreenBtn" class="btn btn-outline-secondary btn-sm" type="button"
10
- aria-label="Toggle fullscreen"><i class="fas fa-expand"></i></button>
11
- <button type="button" class="btn-close" aria-label="Close preview" id="previewCloseBtn">&times;</button>
11
+ <button id="previewFullscreenBtn" class="btn btn-icon btn-outline btn-sm" type="button" aria-label="Toggle fullscreen" title="Fullscreen">
12
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2 5V2h3M14 5V2h-3M2 11v3h3M14 11v3h-3"/></svg>
13
+ </button>
14
+ <button type="button" class="btn-close" aria-label="Close preview" id="previewCloseBtn" onclick="closePreviewModal()">&times;</button>
12
15
  </div>
13
16
  </div>
14
- <div class="modal-body">
17
+ <div class="modal-body p-0">
15
18
  <div class="preview-content" id="previewContent" aria-live="polite">
16
19
  <div class="empty-preview">
17
- <i class="fas fa-spinner fa-spin"></i>
18
- <div class="modal-header-spacing">Loading preview...</div>
20
+ <div class="spinner-border" role="status"></div>
21
+ <div class="modal-header-spacing mt-2">Loading preview...</div>
19
22
  </div>
20
23
  </div>
21
24
  </div>
22
25
  <div class="modal-footer preview-footer">
23
26
  <div class="preview-actions">
24
- <a id="previewOpenBtn" class="btn btn-outline-secondary" target="_blank" rel="noopener noreferrer" href="#"><i
25
- class="fas fa-external-link-alt"></i> Open</a>
26
- <a id="previewDownloadBtn" class="btn btn-success" download href="#"><i class="fas fa-download"></i>
27
- Download</a>
28
- <button type="button" class="btn btn-secondary" id="previewCloseFooter">Close</button>
27
+ <a id="previewOpenBtn" class="btn btn-outline btn-sm" target="_blank" rel="noopener noreferrer" href="#">
28
+ <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M6 3H3a1 1 0 00-1 1v9a1 1 0 001 1h9a1 1 0 001-1v-3M10 2h4v4M9 7l5-5"/></svg>
29
+ Open in new tab
30
+ </a>
31
+ <a id="previewDownloadBtn" class="btn btn-primary btn-sm" download href="#">
32
+ <svg width="13" height="13" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 11v3H2v-3"/><path d="M11 6L8 3 5 6"/><line x1="8" y1="3" x2="8" y2="11"/></svg>
33
+ Download
34
+ </a>
35
+ <button type="button" class="btn btn-outline btn-sm" id="previewCloseFooter" onclick="closePreviewModal()">Close</button>
29
36
  </div>
30
37
  </div>
31
38
  </div>
@@ -0,0 +1,54 @@
1
+ <!-- Upload Modal -->
2
+ <div class="modal" id="uploadModal" tabindex="-1" aria-labelledby="uploadModalLabel" aria-hidden="true">
3
+ <div class="modal-dialog modal-dialog-centered modal-lg">
4
+ <div class="modal-content">
5
+ <div class="modal-header">
6
+ <h5 class="modal-title" id="uploadModalLabel">
7
+ <svg width="18" height="18" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" class="modal-title-icon"><path d="M14 11v3H2v-3"/><path d="M11 6L8 3 5 6"/><line x1="8" y1="3" x2="8" y2="11"/></svg>
8
+ Upload Files
9
+ </h5>
10
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" onclick="closeUploadModal()">&times;</button>
11
+ </div>
12
+ <div class="modal-body">
13
+ <form id="uploadForm" action="/mbkbucket/upload" method="post" enctype="multipart/form-data">
14
+ <input type="hidden" name="prefix" value="{{prefix}}" id="uploadPrefixInput">
15
+
16
+ <!-- Drop Zone -->
17
+ <div id="dropZone" class="upload-drop-zone">
18
+ <div class="drop-zone-content">
19
+ <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" class="drop-icon"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
20
+ <div class="drop-text">Drag files here, or <span class="drop-browse-link">browse</span></div>
21
+ <div class="drop-hint">Supports multiple files &amp; large chunked uploads</div>
22
+ </div>
23
+ <input type="file" class="form-control d-none" id="fileInput" name="files" multiple>
24
+ </div>
25
+
26
+ <!-- Upload Queue List -->
27
+ <div id="uploadQueue" class="upload-queue mt-3"></div>
28
+
29
+ <!-- Upload Progress -->
30
+ <div id="uploadProgress" class="upload-progress-container mt-3" style="display: none;">
31
+ <div class="d-flex justify-content-between align-items-center mb-1">
32
+ <span class="upload-status-label" id="uploadStatusLabel">Uploading...</span>
33
+ <span id="uploadPercent" class="upload-percent">0%</span>
34
+ </div>
35
+ <div class="progress-bar-wrap">
36
+ <div id="progressBar" class="progress-bar-inner" style="width: 0%;"></div>
37
+ </div>
38
+ <div class="upload-meta-info mt-1">
39
+ <small id="uploadDetails" class="text-muted"></small>
40
+ </div>
41
+ </div>
42
+ </form>
43
+ </div>
44
+ <div class="modal-footer">
45
+ <button type="button" class="btn btn-outline" onclick="closeUploadModal()">Cancel</button>
46
+ <button type="button" class="btn btn-outline" id="clearQueueBtn" style="display: none;">Clear Queue</button>
47
+ <button type="button" class="btn btn-primary" id="startUploadBtn">
48
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M14 11v3H2v-3"/><path d="M11 6L8 3 5 6"/><line x1="8" y1="3" x2="8" y2="11"/></svg>
49
+ Start Upload
50
+ </button>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </div>