recycle_bin 1.0.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.
@@ -0,0 +1,229 @@
1
+ <h2>Deleted Items (<%= @deleted_items.count %>)</h2>
2
+
3
+ <!-- Statistics Dashboard -->
4
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px;">
5
+ <div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); border-left: 4px solid #667eea;">
6
+ <div style="font-size: 2rem; font-weight: bold; color: #667eea; margin-bottom: 8px;"><%= @deleted_items.count %></div>
7
+ <div style="color: #6c757d; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.5px;">Items in Trash</div>
8
+ <div style="font-size: 0.8rem; margin-top: 8px; color: #28a745;">
9
+ <%= @deleted_items.select { |item| item.deleted_at > 1.day.ago }.count %> today
10
+ </div>
11
+ </div>
12
+
13
+ <div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); border-left: 4px solid #667eea;">
14
+ <div style="font-size: 2rem; font-weight: bold; color: #667eea; margin-bottom: 8px;"><%= @model_types.count %></div>
15
+ <div style="color: #6c757d; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.5px;">Model Types</div>
16
+ <div style="font-size: 0.8rem; margin-top: 8px;">
17
+ <% if @model_types.any? %>
18
+ <%= @model_types.join(', ') %>
19
+ <% else %>
20
+ No types
21
+ <% end %>
22
+ </div>
23
+ </div>
24
+
25
+ <div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); border-left: 4px solid #667eea;">
26
+ <div style="font-size: 2rem; font-weight: bold; color: #667eea; margin-bottom: 8px;">
27
+ <%= @deleted_items.select { |item| item.deleted_at > 7.days.ago }.count %>
28
+ </div>
29
+ <div style="color: #6c757d; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.5px;">This Week</div>
30
+ <div style="font-size: 0.8rem; margin-top: 8px;">
31
+ Recent activity
32
+ </div>
33
+ </div>
34
+ </div>
35
+
36
+ <!-- Filters -->
37
+ <% if @model_types.any? %>
38
+ <div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px;">
39
+ <div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap;">
40
+ <span style="font-weight: 500; color: #495057;">Filter by type:</span>
41
+ <a href="/recycle_bin/" style="display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border: 1px solid #dee2e6; border-radius: 6px; text-decoration: none; font-size: 14px; font-weight: 500; <%= params[:type].blank? ? 'background: #667eea; color: white;' : 'color: #495057;' %>">All</a>
42
+ <% @model_types.each do |model_type| %>
43
+ <a href="/recycle_bin/?type=<%= model_type %>" style="display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border: 1px solid #dee2e6; border-radius: 6px; text-decoration: none; font-size: 14px; font-weight: 500; <%= params[:type] == model_type ? 'background: #667eea; color: white;' : 'color: #495057;' %>"><%= model_type %></a>
44
+ <% end %>
45
+ </div>
46
+
47
+ <div style="display: flex; gap: 10px; align-items: center; flex-wrap: wrap; margin-top: 15px;">
48
+ <span style="font-weight: 500; color: #495057;">Time:</span>
49
+ <a href="/recycle_bin/?time=today" style="display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border: 1px solid #dee2e6; border-radius: 6px; text-decoration: none; font-size: 14px; font-weight: 500; <%= params[:time] == 'today' ? 'background: #667eea; color: white;' : 'color: #495057;' %>">Today</a>
50
+ <a href="/recycle_bin/?time=week" style="display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border: 1px solid #dee2e6; border-radius: 6px; text-decoration: none; font-size: 14px; font-weight: 500; <%= params[:time] == 'week' ? 'background: #667eea; color: white;' : 'color: #495057;' %>">This Week</a>
51
+ <a href="/recycle_bin/?time=month" style="display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; border: 1px solid #dee2e6; border-radius: 6px; text-decoration: none; font-size: 14px; font-weight: 500; <%= params[:time] == 'month' ? 'background: #667eea; color: white;' : 'color: #495057;' %>">This Month</a>
52
+ </div>
53
+ </div>
54
+ <% end %>
55
+
56
+ <% if @deleted_items.any? %>
57
+ <div style="background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); overflow: hidden;">
58
+
59
+ <!-- Bulk Actions Bar (Initially Hidden) -->
60
+ <div id="bulk-actions" style="display: none; background: #fff3cd; padding: 16px 20px; border-bottom: 1px solid #ffeaa7; justify-content: space-between; align-items: center;">
61
+ <span id="bulk-count" style="font-weight: 500; color: #856404;">0 items selected</span>
62
+ <div style="display: flex; gap: 8px;">
63
+ <%= form_with url: "/recycle_bin/trash/bulk_restore", method: :patch, local: true, style: "display: inline;" do |form| %>
64
+ <input type="hidden" id="bulk-restore-items" name="selected_items" value="">
65
+ <%= form.submit "↶ Restore Selected",
66
+ style: "display: inline-flex; align-items: center; gap: 4px; padding: 8px 16px; background: #28a745; color: white; border: none; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer;",
67
+ onclick: "return handleBulkAction('restore')" %>
68
+ <% end %>
69
+
70
+ <%= form_with url: "/recycle_bin/trash/bulk_destroy", method: :delete, local: true, style: "display: inline;" do |form| %>
71
+ <input type="hidden" id="bulk-destroy-items" name="selected_items" value="">
72
+ <%= form.submit "🗑️ Delete Selected",
73
+ style: "display: inline-flex; align-items: center; gap: 4px; padding: 8px 16px; background: #dc3545; color: white; border: none; border-radius: 6px; font-size: 14px; font-weight: 500; cursor: pointer;",
74
+ onclick: "return handleBulkAction('destroy')" %>
75
+ <% end %>
76
+ </div>
77
+ </div>
78
+
79
+ <table style="width: 100%; border-collapse: collapse;">
80
+ <thead>
81
+ <tr style="background: #f8f9fa; border-bottom: 2px solid #dee2e6;">
82
+ <th style="padding: 16px; text-align: left; font-weight: 600; color: #495057; width: 40px;">
83
+ <input type="checkbox" id="select-all" style="cursor: pointer;" onchange="toggleSelectAll()">
84
+ </th>
85
+ <th style="padding: 16px; text-align: left; font-weight: 600; color: #495057;">Type</th>
86
+ <th style="padding: 16px; text-align: left; font-weight: 600; color: #495057;">Item</th>
87
+ <th style="padding: 16px; text-align: left; font-weight: 600; color: #495057;">Deleted At</th>
88
+ <th style="padding: 16px; text-align: left; font-weight: 600; color: #495057;">Actions</th>
89
+ </tr>
90
+ </thead>
91
+ <tbody>
92
+ <% @deleted_items.each do |item| %>
93
+ <tr style="border-bottom: 1px solid #dee2e6;">
94
+ <td style="padding: 16px;">
95
+ <input type="checkbox" class="item-checkbox" value="<%= item.class.name %>:<%= item.id %>" style="cursor: pointer;" onchange="updateBulkActions()">
96
+ </td>
97
+ <td style="padding: 16px;">
98
+ <span style="background: #667eea; color: white; padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 500;">
99
+ <%= item.class.name %>
100
+ </span>
101
+ </td>
102
+ <td style="padding: 16px;">
103
+ <div style="font-weight: 500; color: #495057; margin-bottom: 4px;">
104
+ <%= truncate(item.recyclable_title, length: 60) %>
105
+ </div>
106
+ <small style="color: #6c757d;">ID: <%= item.id %></small>
107
+ </td>
108
+ <td style="padding: 16px;">
109
+ <div style="font-weight: 500; color: #495057; margin-bottom: 4px;">
110
+ <%= time_ago_in_words(item.deleted_at) %> ago
111
+ </div>
112
+ <small style="color: #6c757d;"><%= item.deleted_at.strftime('%B %d, %Y at %l:%M %p') %></small>
113
+ </td>
114
+ <td style="padding: 16px;">
115
+ <div style="display: flex; gap: 8px; flex-wrap: wrap;">
116
+ <%= form_with url: "/recycle_bin/trash/#{item.class.name}/#{item.id}/restore", method: :patch, local: true, style: "display: inline;" do |form| %>
117
+ <%= form.submit "↶ Restore",
118
+ style: "display: inline-flex; align-items: center; gap: 4px; padding: 6px 12px; background: #28a745; color: white; border: none; border-radius: 6px; text-decoration: none; font-size: 12px; font-weight: 500; cursor: pointer;",
119
+ onclick: "return confirm('Restore this #{item.class.name.downcase}?')" %>
120
+ <% end %>
121
+
122
+ <%= form_with url: "/recycle_bin/trash/#{item.class.name}/#{item.id}", method: :delete, local: true, style: "display: inline;" do |form| %>
123
+ <%= form.submit "🗑️ Delete",
124
+ style: "display: inline-flex; align-items: center; gap: 4px; padding: 6px 12px; background: #dc3545; color: white; border: none; border-radius: 6px; text-decoration: none; font-size: 12px; font-weight: 500; cursor: pointer;",
125
+ onclick: "return confirm('Permanently delete this #{item.class.name.downcase}? This cannot be undone!')" %>
126
+ <% end %>
127
+ </div>
128
+ </td>
129
+ </tr>
130
+ <% end %>
131
+ </tbody>
132
+ </table>
133
+ </div>
134
+
135
+ <div style="text-align: center; padding: 20px; color: #6c757d;">
136
+ Showing <%= @deleted_items.count %> items
137
+ </div>
138
+
139
+ <% else %>
140
+ <div class="empty-state">
141
+ <div style="font-size: 48px; margin-bottom: 20px;">🎉</div>
142
+ <h3>Your recycle bin is empty!</h3>
143
+ <p>Deleted items will appear here and can be restored or permanently removed.</p>
144
+
145
+ <div style="margin-top: 30px;">
146
+ <a href="/recycle_bin/" style="display: inline-flex; align-items: center; gap: 8px; padding: 12px 24px; background: #667eea; color: white; border-radius: 6px; text-decoration: none; font-weight: 500;">
147
+ 🔄 Refresh
148
+ </a>
149
+ </div>
150
+ </div>
151
+ <% end %>
152
+
153
+ <script>
154
+ // Bulk selection functionality
155
+ function toggleSelectAll() {
156
+ const selectAllCheckbox = document.getElementById('select-all');
157
+ const itemCheckboxes = document.querySelectorAll('.item-checkbox');
158
+
159
+ itemCheckboxes.forEach(checkbox => {
160
+ checkbox.checked = selectAllCheckbox.checked;
161
+ });
162
+
163
+ updateBulkActions();
164
+ }
165
+
166
+ function updateBulkActions() {
167
+ const checkedBoxes = document.querySelectorAll('.item-checkbox:checked');
168
+ const bulkActions = document.getElementById('bulk-actions');
169
+ const bulkCount = document.getElementById('bulk-count');
170
+ const selectAllCheckbox = document.getElementById('select-all');
171
+
172
+ if (checkedBoxes.length > 0) {
173
+ bulkActions.style.display = 'flex';
174
+ bulkCount.textContent = checkedBoxes.length + ' item' + (checkedBoxes.length > 1 ? 's' : '') + ' selected';
175
+ } else {
176
+ bulkActions.style.display = 'none';
177
+ }
178
+
179
+ // Update select-all checkbox state
180
+ const itemCheckboxes = document.querySelectorAll('.item-checkbox');
181
+ if (checkedBoxes.length === itemCheckboxes.length && itemCheckboxes.length > 0) {
182
+ selectAllCheckbox.checked = true;
183
+ selectAllCheckbox.indeterminate = false;
184
+ } else if (checkedBoxes.length > 0) {
185
+ selectAllCheckbox.checked = false;
186
+ selectAllCheckbox.indeterminate = true;
187
+ } else {
188
+ selectAllCheckbox.checked = false;
189
+ selectAllCheckbox.indeterminate = false;
190
+ }
191
+ }
192
+
193
+ function handleBulkAction(action) {
194
+ const checkedBoxes = document.querySelectorAll('.item-checkbox:checked');
195
+
196
+ if (checkedBoxes.length === 0) {
197
+ alert('Please select at least one item.');
198
+ return false;
199
+ }
200
+
201
+ const selectedItems = Array.from(checkedBoxes).map(cb => cb.value);
202
+
203
+ // Confirmation message
204
+ let message;
205
+ if (action === 'restore') {
206
+ message = 'Restore ' + checkedBoxes.length + ' selected item' + (checkedBoxes.length > 1 ? 's' : '') + '?';
207
+ } else {
208
+ message = 'Permanently delete ' + checkedBoxes.length + ' selected item' + (checkedBoxes.length > 1 ? 's' : '') + '? This cannot be undone!';
209
+ }
210
+
211
+ if (!confirm(message)) {
212
+ return false;
213
+ }
214
+
215
+ // Set the hidden input values
216
+ if (action === 'restore') {
217
+ document.getElementById('bulk-restore-items').value = JSON.stringify(selectedItems);
218
+ } else {
219
+ document.getElementById('bulk-destroy-items').value = JSON.stringify(selectedItems);
220
+ }
221
+
222
+ return true;
223
+ }
224
+
225
+ // Initialize bulk actions on page load
226
+ document.addEventListener('DOMContentLoaded', function() {
227
+ updateBulkActions();
228
+ });
229
+ </script>
@@ -0,0 +1,288 @@
1
+ <!-- Breadcrumb -->
2
+ <div style="margin-bottom: 2rem;">
3
+ <%= link_to "← Back to Dashboard", recycle_bin.root_path,
4
+ style: "color: #667eea; text-decoration: none; font-weight: 500;" %>
5
+ </div>
6
+
7
+ <!-- Item Header Card -->
8
+ <div class="main-card" style="margin-bottom: 2rem;">
9
+ <div class="card-header">
10
+ <div>
11
+ <h1 class="card-title" style="margin-bottom: 0.5rem;">
12
+ <span class="model-badge" style="margin-right: 1rem;"><%= @item.class.name %></span>
13
+ <%= @item.recyclable_title %>
14
+ </h1>
15
+ <div class="timestamp">
16
+ Deleted <%= time_ago_in_words(@item.deleted_at) %> ago •
17
+ <%= @item.deleted_at.strftime('%B %d, %Y at %l:%M %p') %>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="card-actions">
22
+ <%= link_to recycle_bin.restore_trash_path(@item.class.name, @item),
23
+ method: :patch,
24
+ class: "btn btn-success",
25
+ data: { confirm: "Restore this #{@item.class.name.downcase}?" } do %>
26
+ ↶ Restore Item
27
+ <% end %>
28
+ <%= link_to recycle_bin.destroy_trash_path(@item.class.name, @item),
29
+ method: :delete,
30
+ class: "btn btn-danger",
31
+ data: { confirm: "Permanently delete this #{@item.class.name.downcase}? This cannot be undone!" } do %>
32
+ 🗑️ Delete Forever
33
+ <% end %>
34
+ </div>
35
+ </div>
36
+
37
+ <!-- Status Info -->
38
+ <div style="padding: 1rem 1.5rem; background: #fff3cd; border-bottom: 1px solid #ffeaa7;">
39
+ <div style="display: flex; align-items: center; gap: 1rem;">
40
+ <span style="color: #856404; font-weight: 500;">
41
+ 🗑️ This item is in the recycle bin
42
+ </span>
43
+ <span class="timestamp">
44
+ Size: <%= number_to_human_size(@item_memory_size) %> •
45
+ ID: <%= @item.id %>
46
+ </span>
47
+ </div>
48
+ </div>
49
+ </div>
50
+
51
+ <!-- Original Data -->
52
+ <div class="main-card">
53
+ <div class="card-header">
54
+ <h3 class="card-title">Original Data</h3>
55
+ <div class="card-actions">
56
+ <button class="btn btn-sm btn-outline" onclick="toggleRawData()">
57
+ 📋 Toggle Raw View
58
+ </button>
59
+ </div>
60
+ </div>
61
+
62
+ <div style="padding: 1.5rem;">
63
+ <% if @original_attributes.any? %>
64
+ <div class="table-container">
65
+ <table class="table">
66
+ <thead>
67
+ <tr>
68
+ <th style="width: 30%;">Field</th>
69
+ <th>Value</th>
70
+ <th style="width: 15%;">Type</th>
71
+ </tr>
72
+ </thead>
73
+ <tbody>
74
+ <% @original_attributes.each do |key, value| %>
75
+ <tr>
76
+ <td>
77
+ <strong><%= key.humanize %></strong>
78
+ </td>
79
+ <td>
80
+ <% if value.nil? %>
81
+ <em style="color: #6c757d;">nil</em>
82
+ <% elsif value.is_a?(String) && value.length > 100 %>
83
+ <div>
84
+ <div><%= truncate(value, length: 100) %></div>
85
+ <details style="margin-top: 0.5rem;">
86
+ <summary style="cursor: pointer; color: #667eea; font-size: 0.9rem;">
87
+ Show full content (<%= value.length %> characters)
88
+ </summary>
89
+ <div style="margin-top: 0.5rem; padding: 1rem; background: #f8f9fa; border-radius: 4px; white-space: pre-wrap; font-family: monospace; font-size: 0.85rem;">
90
+ <%= value %>
91
+ </div>
92
+ </details>
93
+ </div>
94
+ <% elsif value.is_a?(Time) || value.is_a?(DateTime) %>
95
+ <div>
96
+ <%= value.strftime('%B %d, %Y at %l:%M %p') %>
97
+ <div class="timestamp">(<%= time_ago_in_words(value) %> ago)</div>
98
+ </div>
99
+ <% elsif value.is_a?(Date) %>
100
+ <%= value.strftime('%B %d, %Y') %>
101
+ <% elsif value == true %>
102
+ <span style="color: #28a745; font-weight: 500;">✓ True</span>
103
+ <% elsif value == false %>
104
+ <span style="color: #dc3545; font-weight: 500;">✗ False</span>
105
+ <% elsif value.is_a?(Numeric) %>
106
+ <span style="font-family: monospace;"><%= number_with_delimiter(value) %></span>
107
+ <% else %>
108
+ <%= value %>
109
+ <% end %>
110
+ </td>
111
+ <td>
112
+ <span class="timestamp">
113
+ <%= value.class.name.downcase %>
114
+ </span>
115
+ </td>
116
+ </tr>
117
+ <% end %>
118
+ </tbody>
119
+ </table>
120
+ </div>
121
+
122
+ <!-- Raw Data View (Hidden by default) -->
123
+ <div id="raw-data" style="display: none; margin-top: 2rem;">
124
+ <div style="background: #f8f9fa; padding: 1rem; border-radius: 4px; border: 1px solid #dee2e6;">
125
+ <h4 style="margin-bottom: 1rem; color: #495057;">Raw JSON Data</h4>
126
+ <pre style="background: #2d3748; color: #e2e8f0; padding: 1rem; border-radius: 4px; overflow-x: auto; font-size: 0.85rem; line-height: 1.4;"><%= JSON.pretty_generate(@original_attributes) %></pre>
127
+ </div>
128
+ </div>
129
+
130
+ <% else %>
131
+ <div class="empty-state">
132
+ <div class="empty-icon">📄</div>
133
+ <h4>No data available</h4>
134
+ <p>This item doesn't have any stored attributes.</p>
135
+ </div>
136
+ <% end %>
137
+ </div>
138
+ </div>
139
+
140
+ <!-- Associations (if any) -->
141
+ <% if @associations&.any? %>
142
+ <div class="main-card" style="margin-top: 2rem;">
143
+ <div class="card-header">
144
+ <h3 class="card-title">Related Items</h3>
145
+ <div class="card-actions">
146
+ <span class="timestamp"><%= @associations.values.flatten.count %> related items</span>
147
+ </div>
148
+ </div>
149
+
150
+ <div style="padding: 1.5rem;">
151
+ <% @associations.each do |association_name, items| %>
152
+ <div style="margin-bottom: 2rem;">
153
+ <h4 style="color: #495057; margin-bottom: 1rem; padding-bottom: 0.5rem; border-bottom: 1px solid #dee2e6;">
154
+ <%= association_name.humanize %>
155
+ <span class="timestamp">(<%= items.count %> items)</span>
156
+ </h4>
157
+
158
+ <% if items.any? %>
159
+ <div style="display: grid; gap: 1rem;">
160
+ <% items.each do |item| %>
161
+ <div style="display: flex; align-items: center; padding: 1rem; background: #f8f9fa; border-radius: 6px; border-left: 3px solid #667eea;">
162
+ <div style="flex: 1;">
163
+ <div style="font-weight: 500; color: #495057;">
164
+ <%= item.class.name %> -
165
+ <%= item.respond_to?(:title) ? item.title :
166
+ item.respond_to?(:name) ? item.name :
167
+ "ID: #{item.id}" %>
168
+ </div>
169
+ <div class="timestamp">
170
+ Created <%= time_ago_in_words(item.created_at) %> ago
171
+ <% if item.respond_to?(:deleted_at) && item.deleted_at %>
172
+ • <span style="color: #dc3545;">Also deleted</span>
173
+ <% end %>
174
+ </div>
175
+ </div>
176
+
177
+ <% if item.respond_to?(:deleted_at) && item.deleted_at %>
178
+ <span class="model-badge" style="background: #dc3545;">Deleted</span>
179
+ <% else %>
180
+ <span class="model-badge" style="background: #28a745;">Active</span>
181
+ <% end %>
182
+ </div>
183
+ <% end %>
184
+ </div>
185
+ <% else %>
186
+ <div class="empty-state" style="padding: 2rem;">
187
+ <p style="color: #6c757d; font-style: italic;">No related items found.</p>
188
+ </div>
189
+ <% end %>
190
+ </div>
191
+ <% end %>
192
+ </div>
193
+ </div>
194
+ <% end %>
195
+
196
+ <!-- Action History -->
197
+ <div class="main-card" style="margin-top: 2rem;">
198
+ <div class="card-header">
199
+ <h3 class="card-title">Action History</h3>
200
+ </div>
201
+
202
+ <div style="padding: 1.5rem;">
203
+ <div style="display: grid; gap: 1rem;">
204
+ <!-- Deletion Event -->
205
+ <div style="display: flex; align-items: start; gap: 1rem; padding: 1rem; border-left: 3px solid #dc3545; background: #fff5f5;">
206
+ <div style="font-size: 1.5rem;">🗑️</div>
207
+ <div style="flex: 1;">
208
+ <div style="font-weight: 500; color: #495057;">Item Deleted</div>
209
+ <div class="timestamp">
210
+ <%= @item.deleted_at.strftime('%B %d, %Y at %l:%M %p') %>
211
+ (<%= time_ago_in_words(@item.deleted_at) %> ago)
212
+ </div>
213
+ <% if @item.respond_to?(:deleted_by) && @item.deleted_by %>
214
+ <div class="timestamp">by <%= @item.deleted_by %></div>
215
+ <% end %>
216
+ </div>
217
+ </div>
218
+
219
+ <!-- Creation Event -->
220
+ <div style="display: flex; align-items: start; gap: 1rem; padding: 1rem; border-left: 3px solid #28a745; background: #f0fff4;">
221
+ <div style="font-size: 1.5rem;">✨</div>
222
+ <div style="flex: 1;">
223
+ <div style="font-weight: 500; color: #495057;">Item Created</div>
224
+ <div class="timestamp">
225
+ <%= @item.created_at.strftime('%B %d, %Y at %l:%M %p') %>
226
+ (<%= time_ago_in_words(@item.created_at) %> ago)
227
+ </div>
228
+ </div>
229
+ </div>
230
+ </div>
231
+ </div>
232
+ </div>
233
+
234
+ <!-- Quick Actions -->
235
+ <div class="main-card" style="margin-top: 2rem; background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);">
236
+ <div style="padding: 2rem; text-align: center;">
237
+ <h4 style="color: #495057; margin-bottom: 1.5rem;">Quick Actions</h4>
238
+
239
+ <div style="display: flex; gap: 1rem; justify-content: center; flex-wrap: wrap;">
240
+ <%= link_to recycle_bin.restore_trash_path(@item.class.name, @item),
241
+ method: :patch,
242
+ class: "btn btn-success",
243
+ data: { confirm: "Restore this #{@item.class.name.downcase}?" } do %>
244
+ ↶ Restore to Application
245
+ <% end %>
246
+
247
+ <%= link_to recycle_bin.destroy_trash_path(@item.class.name, @item),
248
+ method: :delete,
249
+ class: "btn btn-danger",
250
+ data: { confirm: "Permanently delete this #{@item.class.name.downcase}? This cannot be undone!" } do %>
251
+ 🗑️ Delete Permanently
252
+ <% end %>
253
+
254
+ <%= link_to recycle_bin.root_path, class: "btn btn-outline" do %>
255
+ 📋 Back to Dashboard
256
+ <% end %>
257
+
258
+ <button class="btn btn-outline" onclick="window.print()">
259
+ 🖨️ Print Details
260
+ </button>
261
+ </div>
262
+ </div>
263
+ </div>
264
+
265
+ <!-- JavaScript for interactions -->
266
+ <script>
267
+ function toggleRawData() {
268
+ const rawData = document.getElementById('raw-data');
269
+ if (rawData.style.display === 'none') {
270
+ rawData.style.display = 'block';
271
+ } else {
272
+ rawData.style.display = 'none';
273
+ }
274
+ }
275
+
276
+ // Auto-copy functionality for technical details
277
+ document.addEventListener('click', function(e) {
278
+ if (e.target.matches('[data-copy]')) {
279
+ const text = e.target.dataset.copy;
280
+ navigator.clipboard.writeText(text).then(() => {
281
+ e.target.textContent = '✓ Copied!';
282
+ setTimeout(() => {
283
+ e.target.textContent = text;
284
+ }, 2000);
285
+ });
286
+ }
287
+ });
288
+ </script>
data/config/routes.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ RecycleBin::Engine.routes.draw do
4
+ root 'trash#index'
5
+
6
+ # Standard resource routes
7
+ resources :trash, only: [:index] do
8
+ collection do
9
+ patch :bulk_restore
10
+ delete :bulk_destroy
11
+ delete :cleanup_large_items
12
+ end
13
+ end
14
+
15
+ # Custom routes for individual items with model type
16
+ get 'trash/:model_type/:id', to: 'trash#show', as: 'trash'
17
+ patch 'trash/:model_type/:id/restore', to: 'trash#restore', as: 'restore_trash'
18
+ delete 'trash/:model_type/:id', to: 'trash#destroy', as: 'destroy_trash'
19
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/migration'
5
+
6
+ module RecycleBin
7
+ module Generators
8
+ class AddDeletedAtGenerator < ::Rails::Generators::NamedBase
9
+ include Rails::Generators::Migration
10
+
11
+ source_root File.expand_path('templates', __dir__)
12
+ desc 'Add deleted_at column to a model for soft delete functionality'
13
+
14
+ def self.next_migration_number(path)
15
+ next_migration_number = current_migration_number(path) + 1
16
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
17
+ end
18
+
19
+ def create_migration
20
+ migration_template 'add_deleted_at_migration.rb.erb',
21
+ "db/migrate/add_deleted_at_to_#{table_name}.rb"
22
+ end
23
+
24
+ def show_instructions
25
+ say ''
26
+ say 'Next steps:', :green
27
+ say '1. Run the migration: rails db:migrate'
28
+ say "2. Include RecycleBin::SoftDeletable in your #{class_name} model:"
29
+ say ''
30
+ say " class #{class_name} < ApplicationRecord", :yellow
31
+ say ' include RecycleBin::SoftDeletable', :yellow
32
+ say ' end', :yellow
33
+ say ''
34
+ say "3. Your #{class_name.downcase} records will now be soft deleted when you call .destroy"
35
+ say '4. Visit /recycle_bin to manage deleted items'
36
+ end
37
+
38
+ private
39
+
40
+ def table_name
41
+ @table_name ||= name.tableize
42
+ end
43
+
44
+ def class_name
45
+ @class_name ||= name.classify
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,6 @@
1
+ class AddDeletedAtTo<%= table_name.camelize %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
2
+ def change
3
+ add_column :<%= table_name %>, :deleted_at, :datetime
4
+ add_index :<%= table_name %>, :deleted_at
5
+ end
6
+ end