active_storage_dashboard 0.1.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.
- checksums.yaml +7 -0
- data/README.md +91 -0
- data/Rakefile +17 -0
- data/app/controllers/active_storage_dashboard/application_controller.rb +15 -0
- data/app/controllers/active_storage_dashboard/attachments_controller.rb +27 -0
- data/app/controllers/active_storage_dashboard/blobs_controller.rb +55 -0
- data/app/controllers/active_storage_dashboard/dashboard_controller.rb +123 -0
- data/app/controllers/active_storage_dashboard/variant_records_controller.rb +24 -0
- data/app/helpers/active_storage_dashboard/application_helper.rb +289 -0
- data/app/views/active_storage_dashboard/attachments/index.html.erb +269 -0
- data/app/views/active_storage_dashboard/attachments/show.html.erb +163 -0
- data/app/views/active_storage_dashboard/blobs/index.html.erb +262 -0
- data/app/views/active_storage_dashboard/blobs/show.html.erb +203 -0
- data/app/views/active_storage_dashboard/dashboard/index.html.erb +715 -0
- data/app/views/active_storage_dashboard/variant_records/index.html.erb +42 -0
- data/app/views/active_storage_dashboard/variant_records/show.html.erb +59 -0
- data/app/views/layouts/active_storage_dashboard/application.html.erb +302 -0
- data/config/routes.rb +19 -0
- data/lib/active_storage_dashboard/engine.rb +12 -0
- data/lib/active_storage_dashboard/version.rb +5 -0
- data/lib/active_storage_dashboard.rb +8 -0
- data/lib/tasks/active_storage_dashboard_tasks.rake +6 -0
- metadata +79 -0
@@ -0,0 +1,715 @@
|
|
1
|
+
<h1 class="page-title">Active Storage Dashboard</h1>
|
2
|
+
|
3
|
+
<div class="dashboard-overview">
|
4
|
+
<div class="stats-grid">
|
5
|
+
<div class="stat-card">
|
6
|
+
<div class="stat-card-icon blob-icon">
|
7
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
8
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m5.231 13.481L15 17.25m-4.5-15H5.625c-.621 0-1.125.504-1.125 1.125v16.5c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Zm3.75 11.625a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" />
|
9
|
+
</svg>
|
10
|
+
|
11
|
+
|
12
|
+
</div>
|
13
|
+
<div class="stat-card-value count-up" data-value="<%= @blobs_count %>">0</div>
|
14
|
+
<div class="stat-card-label">Total Blobs</div>
|
15
|
+
</div>
|
16
|
+
|
17
|
+
<div class="stat-card">
|
18
|
+
<div class="stat-card-icon attachment-icon">
|
19
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
20
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13" />
|
21
|
+
</svg>
|
22
|
+
|
23
|
+
</div>
|
24
|
+
<div class="stat-card-value count-up" data-value="<%= @attachments_count %>">0</div>
|
25
|
+
<div class="stat-card-label">Total Attachments</div>
|
26
|
+
</div>
|
27
|
+
|
28
|
+
<div class="stat-card">
|
29
|
+
<div class="stat-card-icon variant-icon">
|
30
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
31
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M7.864 4.243A7.5 7.5 0 0 1 19.5 10.5c0 2.92-.556 5.709-1.568 8.268M5.742 6.364A7.465 7.465 0 0 0 4.5 10.5a7.464 7.464 0 0 1-1.15 3.993m1.989 3.559A11.209 11.209 0 0 0 8.25 10.5a3.75 3.75 0 1 1 7.5 0c0 .527-.021 1.049-.064 1.565M12 10.5a14.94 14.94 0 0 1-3.6 9.75m6.633-4.596a18.666 18.666 0 0 1-2.485 5.33" />
|
32
|
+
</svg>
|
33
|
+
|
34
|
+
</div>
|
35
|
+
<div class="stat-card-value count-up" data-value="<%= @variant_records_count %>">0</div>
|
36
|
+
<div class="stat-card-label">Variant Records</div>
|
37
|
+
</div>
|
38
|
+
|
39
|
+
<div class="stat-card">
|
40
|
+
<div class="stat-card-icon storage-icon">
|
41
|
+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4">
|
42
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 0v3.75m-16.5-3.75v3.75m16.5 0v3.75C20.25 16.153 16.556 18 12 18s-8.25-1.847-8.25-4.125v-3.75m16.5 0c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125" />
|
43
|
+
</svg>
|
44
|
+
|
45
|
+
</div>
|
46
|
+
<div class="stat-card-value"><%= format_bytes(@total_storage) %></div>
|
47
|
+
<div class="stat-card-label">Total Storage</div>
|
48
|
+
</div>
|
49
|
+
</div>
|
50
|
+
</div>
|
51
|
+
|
52
|
+
<div class="dashboard-columns">
|
53
|
+
<!-- Left Column: Content Type and Latest Activity -->
|
54
|
+
<div class="dashboard-column">
|
55
|
+
<div class="card">
|
56
|
+
<div class="card-header">
|
57
|
+
<div class="card-header-title">Content Type Distribution</div>
|
58
|
+
</div>
|
59
|
+
<div class="card-body">
|
60
|
+
<% if @content_types.any? %>
|
61
|
+
<div class="content-types-list">
|
62
|
+
<% total = @content_types.sum { |_, count| count } %>
|
63
|
+
<% @content_types.each do |content_type, count| %>
|
64
|
+
<div class="content-type-item">
|
65
|
+
<div class="content-type-info">
|
66
|
+
<span class="content-type-badge"><%= content_type || 'Unknown' %></span>
|
67
|
+
<span class="content-type-count"><%= count %> files</span>
|
68
|
+
</div>
|
69
|
+
<div class="progress-bar">
|
70
|
+
<div class="progress-fill" style="width: <%= (count.to_f / total * 100).round(1) %>%"></div>
|
71
|
+
<div class="progress-text"><%= (count.to_f / total * 100).round(1) %>%</div>
|
72
|
+
</div>
|
73
|
+
</div>
|
74
|
+
<% end %>
|
75
|
+
</div>
|
76
|
+
<% else %>
|
77
|
+
<p class="no-data-message">No content types found.</p>
|
78
|
+
<% end %>
|
79
|
+
</div>
|
80
|
+
</div>
|
81
|
+
|
82
|
+
<div class="card">
|
83
|
+
<div class="card-header">
|
84
|
+
<div class="card-header-title">Latest Activity</div>
|
85
|
+
</div>
|
86
|
+
<div class="card-body">
|
87
|
+
<% if @recent_blobs.any? %>
|
88
|
+
<div class="activity-timeline">
|
89
|
+
<% @recent_blobs.each_with_index do |blob, index| %>
|
90
|
+
<div class="activity-item">
|
91
|
+
<div class="activity-icon">
|
92
|
+
<% if blob.content_type&.start_with?('image/') %>
|
93
|
+
<div class="file-type-icon image-icon">IMG</div>
|
94
|
+
<% elsif blob.content_type&.start_with?('video/') %>
|
95
|
+
<div class="file-type-icon video-icon">VID</div>
|
96
|
+
<% elsif blob.content_type&.start_with?('audio/') %>
|
97
|
+
<div class="file-type-icon audio-icon">AUD</div>
|
98
|
+
<% elsif blob.content_type&.start_with?('application/pdf') %>
|
99
|
+
<div class="file-type-icon pdf-icon">PDF</div>
|
100
|
+
<% else %>
|
101
|
+
<div class="file-type-icon">
|
102
|
+
<%= blob.content_type&.split('/')&.last&.upcase&.first(3) || 'FILE' %>
|
103
|
+
</div>
|
104
|
+
<% end %>
|
105
|
+
</div>
|
106
|
+
<div class="activity-content">
|
107
|
+
<div class="activity-title"><%= blob.filename %></div>
|
108
|
+
<div class="activity-meta">
|
109
|
+
<span class="activity-time"><%= time_ago_in_words(blob.created_at) %> ago</span>
|
110
|
+
<span class="activity-size"><%= format_bytes(blob.byte_size) %></span>
|
111
|
+
</div>
|
112
|
+
<div class="activity-actions">
|
113
|
+
<a href="<%= blob_path(blob) %>" class="btn btn-sm">View Details</a>
|
114
|
+
<a href="<%= download_blob_path(blob) %>" class="btn btn-sm btn-outline">Download</a>
|
115
|
+
</div>
|
116
|
+
</div>
|
117
|
+
</div>
|
118
|
+
<% if index < @recent_blobs.size - 1 %>
|
119
|
+
<div class="activity-divider"></div>
|
120
|
+
<% end %>
|
121
|
+
<% end %>
|
122
|
+
</div>
|
123
|
+
<% else %>
|
124
|
+
<p class="no-data-message">No recent activity found.</p>
|
125
|
+
<% end %>
|
126
|
+
</div>
|
127
|
+
</div>
|
128
|
+
</div>
|
129
|
+
|
130
|
+
<!-- Right Column: Storage Usage and Recent Files -->
|
131
|
+
<div class="dashboard-column">
|
132
|
+
<div class="card">
|
133
|
+
<div class="card-header">
|
134
|
+
<div class="card-header-title">Storage Usage</div>
|
135
|
+
</div>
|
136
|
+
<div class="card-body">
|
137
|
+
<div class="storage-summary">
|
138
|
+
<div class="storage-total">
|
139
|
+
<div class="storage-label">Total Storage Used</div>
|
140
|
+
<div class="storage-value"><%= format_bytes(@total_storage) %></div>
|
141
|
+
</div>
|
142
|
+
|
143
|
+
<div class="storage-metrics">
|
144
|
+
<div class="storage-metric">
|
145
|
+
<div class="storage-metric-title">Average File Size</div>
|
146
|
+
<div class="storage-metric-value"><%= @blobs_count > 0 ? format_bytes(@total_storage / @blobs_count) : '0 B' %></div>
|
147
|
+
</div>
|
148
|
+
<div class="storage-metric">
|
149
|
+
<div class="storage-metric-title">Largest File</div>
|
150
|
+
<div class="storage-metric-value"><%= format_bytes(@largest_blob&.byte_size || 0) %></div>
|
151
|
+
</div>
|
152
|
+
</div>
|
153
|
+
</div>
|
154
|
+
</div>
|
155
|
+
</div>
|
156
|
+
|
157
|
+
<div class="card">
|
158
|
+
<div class="card-header">
|
159
|
+
<div class="card-header-title">Recent Files</div>
|
160
|
+
</div>
|
161
|
+
<div class="card-body">
|
162
|
+
<% if @recent_blobs.any? %>
|
163
|
+
<div class="recent-files-grid">
|
164
|
+
<% @recent_blobs.each do |blob| %>
|
165
|
+
<div class="recent-file-card">
|
166
|
+
<div class="file-preview">
|
167
|
+
<% if blob.content_type&.start_with?('image/') %>
|
168
|
+
<img src="<%= download_blob_path(blob, disposition: 'inline') %>" alt="<%= blob.filename %>" class="file-thumbnail">
|
169
|
+
<% elsif blob.content_type&.start_with?('video/') %>
|
170
|
+
<div class="file-type-icon video-icon">VID</div>
|
171
|
+
<% elsif blob.content_type&.start_with?('audio/') %>
|
172
|
+
<div class="file-type-icon audio-icon">AUD</div>
|
173
|
+
<% elsif blob.content_type&.start_with?('application/pdf') %>
|
174
|
+
<div class="file-type-icon pdf-icon">PDF</div>
|
175
|
+
<% else %>
|
176
|
+
<div class="file-type-icon">
|
177
|
+
<%= blob.content_type&.split('/')&.last&.upcase&.first(3) || 'FILE' %>
|
178
|
+
</div>
|
179
|
+
<% end %>
|
180
|
+
</div>
|
181
|
+
<div class="file-info">
|
182
|
+
<h3 class="file-name" title="<%= blob.filename %>"><%= blob.filename %></h3>
|
183
|
+
<div class="file-meta">
|
184
|
+
<span class="file-size"><%= format_bytes(blob.byte_size) %></span>
|
185
|
+
<span class="file-date"><%= blob.created_at.strftime('%Y-%m-%d %H:%M') %></span>
|
186
|
+
</div>
|
187
|
+
<a href="<%= blob_path(blob) %>" class="view-details-btn">View Details</a>
|
188
|
+
</div>
|
189
|
+
</div>
|
190
|
+
<% end %>
|
191
|
+
</div>
|
192
|
+
<% else %>
|
193
|
+
<p class="no-data-message">No files found.</p>
|
194
|
+
<% end %>
|
195
|
+
</div>
|
196
|
+
</div>
|
197
|
+
</div>
|
198
|
+
</div>
|
199
|
+
|
200
|
+
<style>
|
201
|
+
/* Base styles */
|
202
|
+
:root {
|
203
|
+
--primary-color: #4361ee;
|
204
|
+
--primary-dark-color: #3a0ca3;
|
205
|
+
--secondary-color: #6c757d;
|
206
|
+
--success-color: #2ecc71;
|
207
|
+
--info-color: #3498db;
|
208
|
+
--warning-color: #f39c12;
|
209
|
+
--danger-color: #e74c3c;
|
210
|
+
--light-color: #f8f9fa;
|
211
|
+
--dark-color: #343a40;
|
212
|
+
--card-bg: #ffffff;
|
213
|
+
--border-radius: 0.5rem;
|
214
|
+
--transition-speed: 0.3s;
|
215
|
+
--box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
216
|
+
}
|
217
|
+
|
218
|
+
.page-title {
|
219
|
+
font-size: 2rem;
|
220
|
+
font-weight: 700;
|
221
|
+
margin-bottom: 1.5rem;
|
222
|
+
color: var(--dark-color);
|
223
|
+
position: relative;
|
224
|
+
padding-bottom: 0.5rem;
|
225
|
+
}
|
226
|
+
|
227
|
+
.page-title::after {
|
228
|
+
content: '';
|
229
|
+
position: absolute;
|
230
|
+
bottom: 0;
|
231
|
+
left: 0;
|
232
|
+
width: 60px;
|
233
|
+
height: 4px;
|
234
|
+
background: linear-gradient(90deg, var(--primary-color), var(--primary-dark-color));
|
235
|
+
border-radius: 2px;
|
236
|
+
}
|
237
|
+
|
238
|
+
/* Stats grid */
|
239
|
+
.dashboard-overview {
|
240
|
+
margin-bottom: 2rem;
|
241
|
+
}
|
242
|
+
|
243
|
+
.stats-grid {
|
244
|
+
display: grid;
|
245
|
+
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
246
|
+
gap: 1rem;
|
247
|
+
}
|
248
|
+
|
249
|
+
.stat-card {
|
250
|
+
background-color: var(--card-bg);
|
251
|
+
border-radius: var(--border-radius);
|
252
|
+
box-shadow: var(--box-shadow);
|
253
|
+
padding: 1.5rem;
|
254
|
+
text-align: center;
|
255
|
+
transition: transform var(--transition-speed), box-shadow var(--transition-speed);
|
256
|
+
position: relative;
|
257
|
+
overflow: hidden;
|
258
|
+
}
|
259
|
+
|
260
|
+
.stat-card:hover {
|
261
|
+
transform: translateY(-5px);
|
262
|
+
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
263
|
+
}
|
264
|
+
|
265
|
+
.stat-card::after {
|
266
|
+
content: '';
|
267
|
+
position: absolute;
|
268
|
+
top: 0;
|
269
|
+
left: 0;
|
270
|
+
width: 100%;
|
271
|
+
height: 4px;
|
272
|
+
background: linear-gradient(90deg, var(--primary-color), var(--info-color));
|
273
|
+
}
|
274
|
+
|
275
|
+
.stat-card-icon {
|
276
|
+
display: flex;
|
277
|
+
padding: 10px;
|
278
|
+
align-items: center;
|
279
|
+
justify-content: center;
|
280
|
+
width: 48px;
|
281
|
+
height: 48px;
|
282
|
+
margin: 0 auto 1rem;
|
283
|
+
border-radius: 50%;
|
284
|
+
color: white;
|
285
|
+
}
|
286
|
+
|
287
|
+
.blob-icon {
|
288
|
+
background-color: var(--primary-color);
|
289
|
+
}
|
290
|
+
|
291
|
+
.attachment-icon {
|
292
|
+
background-color: var(--success-color);
|
293
|
+
}
|
294
|
+
|
295
|
+
.variant-icon {
|
296
|
+
background-color: var(--warning-color);
|
297
|
+
}
|
298
|
+
|
299
|
+
.storage-icon {
|
300
|
+
background-color: var(--info-color);
|
301
|
+
}
|
302
|
+
|
303
|
+
.stat-card-value {
|
304
|
+
font-size: 2.5rem;
|
305
|
+
font-weight: 700;
|
306
|
+
color: var(--dark-color);
|
307
|
+
margin-bottom: 0.5rem;
|
308
|
+
line-height: 1.2;
|
309
|
+
}
|
310
|
+
|
311
|
+
.stat-card-label {
|
312
|
+
color: var(--secondary-color);
|
313
|
+
font-size: 0.875rem;
|
314
|
+
text-transform: uppercase;
|
315
|
+
letter-spacing: 0.05em;
|
316
|
+
font-weight: 500;
|
317
|
+
}
|
318
|
+
|
319
|
+
/* Dashboard columns layout */
|
320
|
+
.dashboard-columns {
|
321
|
+
display: grid;
|
322
|
+
grid-template-columns: 1fr;
|
323
|
+
gap: 1.5rem;
|
324
|
+
}
|
325
|
+
|
326
|
+
@media (min-width: 992px) {
|
327
|
+
.dashboard-columns {
|
328
|
+
grid-template-columns: 1fr 1fr;
|
329
|
+
}
|
330
|
+
}
|
331
|
+
|
332
|
+
.dashboard-column {
|
333
|
+
display: flex;
|
334
|
+
flex-direction: column;
|
335
|
+
gap: 1.5rem;
|
336
|
+
}
|
337
|
+
|
338
|
+
/* Cards */
|
339
|
+
.card {
|
340
|
+
background-color: var(--card-bg);
|
341
|
+
border-radius: var(--border-radius);
|
342
|
+
box-shadow: var(--box-shadow);
|
343
|
+
overflow: hidden;
|
344
|
+
transition: box-shadow var(--transition-speed);
|
345
|
+
}
|
346
|
+
|
347
|
+
.card:hover {
|
348
|
+
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1);
|
349
|
+
}
|
350
|
+
|
351
|
+
.card-header {
|
352
|
+
padding: 1rem 1.5rem;
|
353
|
+
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
354
|
+
background-color: rgba(0, 0, 0, 0.02);
|
355
|
+
}
|
356
|
+
|
357
|
+
.card-header-title {
|
358
|
+
font-weight: 600;
|
359
|
+
font-size: 1.1rem;
|
360
|
+
color: var(--dark-color);
|
361
|
+
}
|
362
|
+
|
363
|
+
.card-body {
|
364
|
+
padding: 1.5rem;
|
365
|
+
}
|
366
|
+
|
367
|
+
/* Content types */
|
368
|
+
.content-types-list {
|
369
|
+
display: flex;
|
370
|
+
flex-direction: column;
|
371
|
+
gap: 1.25rem;
|
372
|
+
}
|
373
|
+
|
374
|
+
.content-type-item {
|
375
|
+
display: flex;
|
376
|
+
flex-direction: column;
|
377
|
+
gap: 0.5rem;
|
378
|
+
}
|
379
|
+
|
380
|
+
.content-type-info {
|
381
|
+
display: flex;
|
382
|
+
justify-content: space-between;
|
383
|
+
align-items: center;
|
384
|
+
}
|
385
|
+
|
386
|
+
.content-type-badge {
|
387
|
+
display: inline-block;
|
388
|
+
padding: 0.25rem 0.5rem;
|
389
|
+
background-color: var(--light-color);
|
390
|
+
border-radius: 0.25rem;
|
391
|
+
font-size: 0.875rem;
|
392
|
+
font-family: monospace;
|
393
|
+
}
|
394
|
+
|
395
|
+
.content-type-count {
|
396
|
+
font-size: 0.875rem;
|
397
|
+
color: var(--secondary-color);
|
398
|
+
}
|
399
|
+
|
400
|
+
.progress-bar {
|
401
|
+
height: 8px;
|
402
|
+
background-color: var(--light-color);
|
403
|
+
border-radius: 4px;
|
404
|
+
overflow: hidden;
|
405
|
+
position: relative;
|
406
|
+
margin-top: 5px;
|
407
|
+
width: 100%;
|
408
|
+
}
|
409
|
+
|
410
|
+
.progress-fill {
|
411
|
+
height: 100%;
|
412
|
+
background: linear-gradient(90deg, var(--primary-color), var(--info-color));
|
413
|
+
border-radius: 4px;
|
414
|
+
transition: width 1s ease-in-out;
|
415
|
+
}
|
416
|
+
|
417
|
+
.progress-text {
|
418
|
+
position: absolute;
|
419
|
+
right: 0;
|
420
|
+
top: -18px;
|
421
|
+
font-size: 0.75rem;
|
422
|
+
color: var(--secondary-color);
|
423
|
+
}
|
424
|
+
|
425
|
+
/* Activity timeline */
|
426
|
+
.activity-timeline {
|
427
|
+
display: flex;
|
428
|
+
flex-direction: column;
|
429
|
+
}
|
430
|
+
|
431
|
+
.activity-item {
|
432
|
+
display: flex;
|
433
|
+
gap: 1rem;
|
434
|
+
padding: 0.75rem 0;
|
435
|
+
}
|
436
|
+
|
437
|
+
.activity-divider {
|
438
|
+
height: 1.5rem;
|
439
|
+
width: 2px;
|
440
|
+
background-color: var(--light-color);
|
441
|
+
margin-left: 20px;
|
442
|
+
}
|
443
|
+
|
444
|
+
.activity-icon {
|
445
|
+
flex-shrink: 0;
|
446
|
+
}
|
447
|
+
|
448
|
+
.activity-content {
|
449
|
+
flex-grow: 1;
|
450
|
+
}
|
451
|
+
|
452
|
+
.activity-title {
|
453
|
+
font-weight: 500;
|
454
|
+
margin-bottom: 0.25rem;
|
455
|
+
white-space: nowrap;
|
456
|
+
overflow: hidden;
|
457
|
+
text-overflow: ellipsis;
|
458
|
+
}
|
459
|
+
|
460
|
+
.activity-meta {
|
461
|
+
font-size: 0.8rem;
|
462
|
+
color: var(--secondary-color);
|
463
|
+
display: flex;
|
464
|
+
gap: 1rem;
|
465
|
+
margin-bottom: 0.5rem;
|
466
|
+
}
|
467
|
+
|
468
|
+
.activity-actions {
|
469
|
+
display: flex;
|
470
|
+
gap: 0.5rem;
|
471
|
+
}
|
472
|
+
|
473
|
+
/* Storage summary */
|
474
|
+
.storage-summary {
|
475
|
+
display: flex;
|
476
|
+
flex-direction: column;
|
477
|
+
gap: 2rem;
|
478
|
+
}
|
479
|
+
|
480
|
+
.storage-total {
|
481
|
+
text-align: center;
|
482
|
+
padding: 1rem;
|
483
|
+
background-color: rgba(53, 152, 219, 0.1);
|
484
|
+
border-radius: var(--border-radius);
|
485
|
+
}
|
486
|
+
|
487
|
+
.storage-label {
|
488
|
+
font-size: 0.9rem;
|
489
|
+
color: var(--secondary-color);
|
490
|
+
margin-bottom: 0.5rem;
|
491
|
+
}
|
492
|
+
|
493
|
+
.storage-value {
|
494
|
+
font-size: 2rem;
|
495
|
+
font-weight: 700;
|
496
|
+
color: var(--primary-color);
|
497
|
+
}
|
498
|
+
|
499
|
+
.storage-metrics {
|
500
|
+
display: grid;
|
501
|
+
grid-template-columns: 1fr 1fr;
|
502
|
+
gap: 1rem;
|
503
|
+
}
|
504
|
+
|
505
|
+
.storage-metric {
|
506
|
+
text-align: center;
|
507
|
+
padding: 1rem;
|
508
|
+
background-color: var(--light-color);
|
509
|
+
border-radius: var(--border-radius);
|
510
|
+
}
|
511
|
+
|
512
|
+
.storage-metric-title {
|
513
|
+
font-size: 0.8rem;
|
514
|
+
color: var(--secondary-color);
|
515
|
+
margin-bottom: 0.5rem;
|
516
|
+
}
|
517
|
+
|
518
|
+
.storage-metric-value {
|
519
|
+
font-size: 1.2rem;
|
520
|
+
font-weight: 600;
|
521
|
+
color: var(--dark-color);
|
522
|
+
}
|
523
|
+
|
524
|
+
/* Recent files */
|
525
|
+
.recent-files-grid {
|
526
|
+
display: grid;
|
527
|
+
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
528
|
+
gap: 1rem;
|
529
|
+
}
|
530
|
+
|
531
|
+
.recent-file-card {
|
532
|
+
background-color: var(--light-color);
|
533
|
+
border-radius: var(--border-radius);
|
534
|
+
overflow: hidden;
|
535
|
+
display: flex;
|
536
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
537
|
+
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
538
|
+
}
|
539
|
+
|
540
|
+
.recent-file-card:hover {
|
541
|
+
transform: translateY(-5px);
|
542
|
+
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
543
|
+
}
|
544
|
+
|
545
|
+
.file-preview {
|
546
|
+
width: 80px;
|
547
|
+
height: 100%;
|
548
|
+
display: flex;
|
549
|
+
align-items: center;
|
550
|
+
justify-content: center;
|
551
|
+
background-color: #e9ecef;
|
552
|
+
}
|
553
|
+
|
554
|
+
.file-thumbnail {
|
555
|
+
width: 100%;
|
556
|
+
height: 100%;
|
557
|
+
object-fit: cover;
|
558
|
+
}
|
559
|
+
|
560
|
+
.file-type-icon {
|
561
|
+
width: 40px;
|
562
|
+
height: 40px;
|
563
|
+
display: flex;
|
564
|
+
align-items: center;
|
565
|
+
justify-content: center;
|
566
|
+
border-radius: 4px;
|
567
|
+
font-weight: bold;
|
568
|
+
color: white;
|
569
|
+
background-color: #6c757d;
|
570
|
+
font-size: 0.7rem;
|
571
|
+
}
|
572
|
+
|
573
|
+
.image-icon {
|
574
|
+
background-color: #3498db;
|
575
|
+
}
|
576
|
+
|
577
|
+
.video-icon {
|
578
|
+
background-color: #e74c3c;
|
579
|
+
}
|
580
|
+
|
581
|
+
.audio-icon {
|
582
|
+
background-color: #9b59b6;
|
583
|
+
}
|
584
|
+
|
585
|
+
.pdf-icon {
|
586
|
+
background-color: #e67e22;
|
587
|
+
}
|
588
|
+
|
589
|
+
.file-info {
|
590
|
+
padding: 0.75rem;
|
591
|
+
flex: 1;
|
592
|
+
}
|
593
|
+
|
594
|
+
.file-name {
|
595
|
+
font-size: 0.9rem;
|
596
|
+
margin: 0 0 0.5rem 0;
|
597
|
+
white-space: nowrap;
|
598
|
+
overflow: hidden;
|
599
|
+
text-overflow: ellipsis;
|
600
|
+
}
|
601
|
+
|
602
|
+
.file-meta {
|
603
|
+
display: flex;
|
604
|
+
justify-content: space-between;
|
605
|
+
color: var(--secondary-color);
|
606
|
+
font-size: 0.75rem;
|
607
|
+
margin-bottom: 0.5rem;
|
608
|
+
}
|
609
|
+
|
610
|
+
.view-details-btn {
|
611
|
+
display: inline-block;
|
612
|
+
padding: 0.25rem 0.5rem;
|
613
|
+
background-color: var(--primary-color);
|
614
|
+
color: white;
|
615
|
+
border-radius: 0.25rem;
|
616
|
+
text-decoration: none;
|
617
|
+
font-size: 0.75rem;
|
618
|
+
transition: background-color 0.2s;
|
619
|
+
text-align: center;
|
620
|
+
}
|
621
|
+
|
622
|
+
.view-details-btn:hover {
|
623
|
+
background-color: var(--primary-dark-color);
|
624
|
+
}
|
625
|
+
|
626
|
+
/* Buttons */
|
627
|
+
.btn {
|
628
|
+
display: inline-block;
|
629
|
+
padding: 0.375rem 0.75rem;
|
630
|
+
background-color: var(--primary-color);
|
631
|
+
color: white;
|
632
|
+
border-radius: 0.25rem;
|
633
|
+
text-decoration: none;
|
634
|
+
font-size: 0.875rem;
|
635
|
+
transition: background-color 0.2s;
|
636
|
+
border: none;
|
637
|
+
cursor: pointer;
|
638
|
+
}
|
639
|
+
|
640
|
+
.btn:hover {
|
641
|
+
background-color: var(--primary-dark-color);
|
642
|
+
}
|
643
|
+
|
644
|
+
.btn-sm {
|
645
|
+
padding: 0.25rem 0.5rem;
|
646
|
+
font-size: 0.75rem;
|
647
|
+
}
|
648
|
+
|
649
|
+
.btn-outline {
|
650
|
+
background-color: transparent;
|
651
|
+
border: 1px solid var(--primary-color);
|
652
|
+
color: var(--primary-color);
|
653
|
+
}
|
654
|
+
|
655
|
+
.btn-outline:hover {
|
656
|
+
background-color: var(--primary-color);
|
657
|
+
color: white;
|
658
|
+
}
|
659
|
+
|
660
|
+
/* No data message */
|
661
|
+
.no-data-message {
|
662
|
+
text-align: center;
|
663
|
+
color: var(--secondary-color);
|
664
|
+
font-style: italic;
|
665
|
+
padding: 2rem 0;
|
666
|
+
}
|
667
|
+
</style>
|
668
|
+
|
669
|
+
<script>
|
670
|
+
document.addEventListener('DOMContentLoaded', function() {
|
671
|
+
// CountUp animation for stat values
|
672
|
+
document.querySelectorAll('.count-up').forEach(function(element) {
|
673
|
+
const target = parseInt(element.getAttribute('data-value'));
|
674
|
+
const duration = 1500;
|
675
|
+
const step = Math.ceil(target / (duration / 16)); // 16ms per frame (approx 60fps)
|
676
|
+
let current = 0;
|
677
|
+
|
678
|
+
function updateCount() {
|
679
|
+
current += step;
|
680
|
+
if (current >= target) {
|
681
|
+
element.textContent = target.toLocaleString();
|
682
|
+
return;
|
683
|
+
}
|
684
|
+
element.textContent = current.toLocaleString();
|
685
|
+
requestAnimationFrame(updateCount);
|
686
|
+
}
|
687
|
+
|
688
|
+
updateCount();
|
689
|
+
});
|
690
|
+
|
691
|
+
// Animate progress bars on scroll
|
692
|
+
const progressBars = document.querySelectorAll('.progress-fill');
|
693
|
+
|
694
|
+
const animateProgressBars = () => {
|
695
|
+
progressBars.forEach(bar => {
|
696
|
+
const rect = bar.getBoundingClientRect();
|
697
|
+
const isVisible = (rect.top <= window.innerHeight && rect.bottom >= 0);
|
698
|
+
|
699
|
+
if (isVisible) {
|
700
|
+
const width = bar.style.width;
|
701
|
+
bar.style.width = '0%';
|
702
|
+
setTimeout(() => {
|
703
|
+
bar.style.width = width;
|
704
|
+
}, 100);
|
705
|
+
}
|
706
|
+
});
|
707
|
+
};
|
708
|
+
|
709
|
+
// Initial animation
|
710
|
+
animateProgressBars();
|
711
|
+
|
712
|
+
// Animate on scroll
|
713
|
+
window.addEventListener('scroll', animateProgressBars);
|
714
|
+
});
|
715
|
+
</script>
|