jekyll-theme-zer0 0.4.0 → 0.5.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.
@@ -1,123 +1,950 @@
1
- <!--
2
- file: sitemap.html
3
- path: _includes/content/sitemap.html
4
- description: This is the snippet for the sitemap page.
5
- -->
6
-
7
- <h1>Sitemap - Index</h1>
8
-
9
- <input type="text" id="searchBar" onkeyup="searchFunction()" placeholder="Search for keywords..">
10
-
11
- <div class="table-responsive">
12
- <table class="table">
13
- <thead>
14
- <tr>
15
- <th scope="col" data-type="string" data-order="desc">Collection <i class="sort-icon"></i></th>
16
- <th scope="col" data-type="string" data-order="desc">Page <i class="sort-icon"></i></th>
17
- <th scope="col" data-type="string" data-order="desc">Description<i class="sort-icon"></i></th>
18
- <th scope="col" data-type="string" data-order="desc">Excerpt<i class="sort-icon"></i></th>
19
- <th scope="col" data-type="string" data-order="desc">Path<i class="sort-icon"></i></th>
20
- <th scope="col" data-type="date" data-order="desc">Last Modified Date <i class="sort-icon"></i></th>
21
- <th scope="col" data-type="string" data-order="desc">Categories<i class="sort-icon"></i></th>
22
- <th scope="col" data-type="string" data-order="desc">Tags<i class="sort-icon"></i></th>
23
- <th scope="col" data-type="string" data-order="desc">Absolute URL<i class="sort-icon"></i></th>
24
- <th scope="col" data-type="string" data-order="desc">Author <i class="sort-icon"></i></th>
1
+ <!--
2
+ ===================================================================
3
+ SITEMAP COMPONENT - Comprehensive Site Navigation Index
4
+ ===================================================================
5
+
6
+ File: sitemap.html
7
+ Path: _includes/content/sitemap.html
8
+ Purpose: Interactive sitemap with advanced search, filtering, and sorting
9
+
10
+ Features:
11
+ - Real-time search across all content fields
12
+ - Multi-column sorting with visual indicators
13
+ - Collection-based filtering
14
+ - Responsive table design with Bootstrap styling
15
+ - URL query parameter support for direct search
16
+ - Statistics overview and content discovery tools
17
+
18
+ Dependencies:
19
+ - Bootstrap 5 for responsive table and styling
20
+ - Bootstrap Icons for visual elements
21
+ - Jekyll collections and site data
22
+
23
+ Enhanced Functionality:
24
+ - Advanced search with debouncing
25
+ - Collection filters and statistics
26
+ - Export capabilities (future enhancement)
27
+ - Keyboard navigation support
28
+ ===================================================================
29
+ -->
30
+
31
+ <!-- ================================ -->
32
+ <!-- SITEMAP OVERVIEW AND STATS -->
33
+ <!-- ================================ -->
34
+ <div class="row mb-4">
35
+ <div class="col-12">
36
+ <div class="card border-0">
37
+ <div class="card-body">
38
+ <div class="row align-items-center">
39
+ <div class="col-md-8">
40
+ <h4 class="card-title mb-2">
41
+ <i class="bi bi-map text-primary"></i>
42
+ Site Navigation Index
43
+ </h4>
44
+ <p class="card-text text-muted mb-0">
45
+ Explore all <span id="totalPages" class="fw-bold text-info">0</span> pages across
46
+ <span id="totalCollections" class="fw-bold text-info">0</span> collections on this site.
47
+ </p>
48
+ </div>
49
+ <div class="col-md-4 text-md-end">
50
+ <div class="btn-group" role="group" aria-label="View options">
51
+ <button type="button" class="btn btn-outline-secondary" id="toggleStats">
52
+ <i class="bi bi-bar-chart"></i> Stats
53
+ </button>
54
+ <button type="button" class="btn btn-outline-secondary" id="resetFilters">
55
+ <i class="bi bi-arrow-clockwise"></i> Reset
56
+ </button>
57
+ </div>
58
+ </div>
59
+ </div>
60
+ </div>
61
+ </div>
62
+ </div>
63
+ </div>
64
+
65
+ <!-- ================================ -->
66
+ <!-- COLLECTION STATISTICS -->
67
+ <!-- ================================ -->
68
+ <div class="row mb-4 d-none" id="statsSection">
69
+ <div class="col-12">
70
+ <div class="card">
71
+ <div class="card-header">
72
+ <h5 class="mb-0">
73
+ <i class="bi bi-pie-chart"></i>
74
+ Content Statistics
75
+ </h5>
76
+ </div>
77
+ <div class="card-body">
78
+ <div class="row" id="collectionStats">
79
+ <!-- Dynamic collection statistics will be inserted here -->
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+
86
+ <!-- ================================ -->
87
+ <!-- SEARCH AND FILTER CONTROLS -->
88
+ <!-- ================================ -->
89
+ <div class="row mb-4">
90
+ <div class="col-12">
91
+ <div class="card">
92
+ <div class="card-body">
93
+ <div class="row g-3">
94
+ <!-- Search Input -->
95
+ <div class="col-md-6">
96
+ <label for="searchBar" class="form-label">
97
+ <i class="bi bi-search"></i> Search Content
98
+ </label>
99
+ <div class="input-group">
100
+ <input type="text"
101
+ class="form-control"
102
+ id="searchBar"
103
+ placeholder="Search titles, descriptions, tags, or any content..."
104
+ aria-describedby="searchHelp">
105
+ <button class="btn btn-outline-secondary"
106
+ type="button"
107
+ id="clearSearch"
108
+ title="Clear search"
109
+ aria-label="Clear search">
110
+ <i class="bi bi-x-lg"></i>
111
+ </button>
112
+ </div>
113
+ <div id="searchHelp" class="form-text">
114
+ Search across titles, descriptions, categories, tags, and more
115
+ </div>
116
+ </div>
117
+
118
+ <!-- Collection Filter -->
119
+ <div class="col-md-3">
120
+ <label for="collectionFilter" class="form-label">
121
+ <i class="bi bi-funnel"></i> Filter by Collection
122
+ </label>
123
+ <select class="form-select" id="collectionFilter">
124
+ <option value="">All Collections</option>
125
+ <!-- Dynamic options will be added by JavaScript -->
126
+ </select>
127
+ </div>
128
+
129
+ <!-- Date Range Filter -->
130
+ <div class="col-md-3">
131
+ <label for="dateFilter" class="form-label">
132
+ <i class="bi bi-calendar-range"></i> Date Range
133
+ </label>
134
+ <select class="form-select" id="dateFilter">
135
+ <option value="">All Dates</option>
136
+ <option value="today">Today</option>
137
+ <option value="week">This Week</option>
138
+ <option value="month">This Month</option>
139
+ <option value="year">This Year</option>
140
+ </select>
141
+ </div>
142
+ </div>
25
143
 
26
- </tr>
27
- </thead>
28
- <tbody>
29
- {% for collection in site.collections %}
30
- {% for item in collection.docs %}
31
- <tr>
32
- <td>{{ collection.label }}</td>
33
- <td>
34
- <a href="{{ item.url | relative_url }}">{{ item.title }}</a>
35
- </td>
36
- <td>{{ item.description }}</td>
37
- <td>{{ item.excerpt }}</td>
38
- <td>{{ item.path }}</td>
39
- <td>{{ item.lastmod | date: "%Y/%m/%d" | default: "null" }}</td>
40
- <td>{{ item.categories | join: ", " }}</td>
41
- <td>{{ item.tags | join: ", " }}</td>
42
- <td>
43
- <a href="{{ site.url }}{{ item.url }}">{{ site.url }}{{ item.url }}</a>
44
- </td>
45
- <td>{{ item.author }}</td>
46
- </tr>
47
- {% endfor %}
48
- {% endfor %}
49
- </tbody>
50
- </table>
144
+ <!-- Results Counter -->
145
+ <div class="row mt-3">
146
+ <div class="col-12">
147
+ <div class="d-flex justify-content-between align-items-center">
148
+ <small class="text-muted">
149
+ Showing <span id="visibleCount" class="fw-bold">0</span> of
150
+ <span id="totalCount" class="fw-bold">0</span> pages
151
+ </small>
152
+ <div class="btn-group btn-group-sm" role="group" aria-label="Display options">
153
+ <input type="radio" class="btn-check" name="viewMode" id="tableView" checked>
154
+ <label class="btn btn-outline-primary" for="tableView">
155
+ <i class="bi bi-table"></i> Table
156
+ </label>
157
+ <input type="radio" class="btn-check" name="viewMode" id="cardView">
158
+ <label class="btn btn-outline-primary" for="cardView">
159
+ <i class="bi bi-grid"></i> Cards
160
+ </label>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </div>
51
168
  </div>
52
169
 
53
- <!-- Sorting feature -->
170
+ <!-- ================================ -->
171
+ <!-- RESPONSIVE DATA TABLE -->
172
+ <!-- ================================ -->
173
+ <div class="row" id="tableViewContainer">
174
+ <div class="col-12">
175
+ <div class="card">
176
+ <div class="card-body p-0">
177
+ <div class="table-responsive">
178
+ <table class="table table-hover mb-0" id="sitemapTable">
179
+ <thead class="table-dark sticky-top">
180
+ <tr>
181
+ <th scope="col" data-type="string" data-order="asc" class="sitemap-col-collection">
182
+ Collection
183
+ <i class="sort-icon bi bi-arrow-down-up ms-1"></i>
184
+ </th>
185
+ <th scope="col" data-type="string" data-order="asc" class="sitemap-col-title">
186
+ Page Title
187
+ <i class="sort-icon bi bi-arrow-down-up ms-1"></i>
188
+ </th>
189
+ <th scope="col" data-type="string" data-order="asc" class="d-none d-md-table-cell">
190
+ Description
191
+ <i class="sort-icon bi bi-arrow-down-up ms-1"></i>
192
+ </th>
193
+ <th scope="col" data-type="date" data-order="desc" class="sitemap-col-date">
194
+ Last Modified
195
+ <i class="sort-icon bi bi-arrow-down-up ms-1"></i>
196
+ </th>
197
+ <th scope="col" data-type="string" data-order="asc" class="d-none d-lg-table-cell">
198
+ Categories
199
+ <i class="sort-icon bi bi-arrow-down-up ms-1"></i>
200
+ </th>
201
+ <th scope="col" data-type="string" data-order="asc" class="d-none d-xl-table-cell">
202
+ Tags
203
+ <i class="sort-icon bi bi-arrow-down-up ms-1"></i>
204
+ </th>
205
+ <th scope="col" class="text-center sitemap-col-actions">
206
+ Actions
207
+ </th>
208
+ </tr>
209
+ </thead>
210
+ <tbody id="sitemapTableBody">
211
+ {% assign total_pages = 0 %}
212
+ {% assign collections_data = '' %}
213
+
214
+ {% comment %} Include regular pages {% endcomment %}
215
+ {% for page in site.pages %}
216
+ {% unless page.sitemap == false %}
217
+ {% assign total_pages = total_pages | plus: 1 %}
218
+ <tr data-collection="pages"
219
+ data-title="{{ page.title | default: page.name | escape }}"
220
+ data-description="{{ page.description | escape }}"
221
+ data-categories="{{ page.categories | join: ', ' | escape }}"
222
+ data-tags="{{ page.tags | join: ', ' | escape }}"
223
+ data-date="{{ page.lastmod | default: page.date | date: '%Y-%m-%d' }}"
224
+ data-url="{{ page.url | relative_url }}">
225
+
226
+ <td>
227
+ <span class="badge bg-primary rounded-pill">Pages</span>
228
+ </td>
229
+ <td>
230
+ <a href="{{ page.url | relative_url }}"
231
+ class="text-decoration-none fw-medium">
232
+ {{ page.title | default: page.name | truncate: 50 }}
233
+ </a>
234
+ {% if page.description %}
235
+ <br><small class="text-muted d-md-none">{{ page.description | truncate: 80 }}</small>
236
+ {% endif %}
237
+ </td>
238
+ <td class="d-none d-md-table-cell">
239
+ <small class="text-muted">
240
+ {{ page.description | default: "No description available" | truncate: 120 }}
241
+ </small>
242
+ </td>
243
+ <td>
244
+ <small class="text-muted">
245
+ {{ page.lastmod | default: page.date | date: "%b %d, %Y" | default: "Unknown" }}
246
+ </small>
247
+ </td>
248
+ <td class="d-none d-lg-table-cell">
249
+ {% if page.categories and page.categories.size > 0 %}
250
+ {% for category in page.categories limit: 2 %}
251
+ <span class="badge bg-secondary me-1">{{ category }}</span>
252
+ {% endfor %}
253
+ {% if page.categories.size > 2 %}
254
+ <span class="badge bg-light text-dark">+{{ page.categories.size | minus: 2 }}</span>
255
+ {% endif %}
256
+ {% endif %}
257
+ </td>
258
+ <td class="d-none d-xl-table-cell">
259
+ {% if page.tags and page.tags.size > 0 %}
260
+ {% for tag in page.tags limit: 3 %}
261
+ <span class="badge bg-outline-primary me-1">#{{ tag }}</span>
262
+ {% endfor %}
263
+ {% if page.tags.size > 3 %}
264
+ <span class="badge bg-light text-dark">+{{ page.tags.size | minus: 3 }}</span>
265
+ {% endif %}
266
+ {% endif %}
267
+ </td>
268
+ <td class="text-center">
269
+ <div class="btn-group btn-group-sm" role="group">
270
+ <a href="{{ page.url | relative_url }}"
271
+ class="btn btn-outline-primary btn-sm"
272
+ title="View page">
273
+ <i class="bi bi-eye"></i>
274
+ </a>
275
+ <button class="btn btn-outline-secondary btn-sm copy-url"
276
+ data-url="{{ site.url }}{{ page.url }}"
277
+ title="Copy URL">
278
+ <i class="bi bi-clipboard"></i>
279
+ </button>
280
+ </div>
281
+ </td>
282
+ </tr>
283
+ {% endunless %}
284
+ {% endfor %}
285
+
286
+ {% comment %} Include collection documents {% endcomment %}
287
+ {% for collection in site.collections %}
288
+ {% for item in collection.docs %}
289
+ {% unless item.sitemap == false %}
290
+ {% assign total_pages = total_pages | plus: 1 %}
291
+ <tr data-collection="{{ collection.label }}"
292
+ data-title="{{ item.title | escape }}"
293
+ data-description="{{ item.description | escape }}"
294
+ data-categories="{{ item.categories | join: ', ' | escape }}"
295
+ data-tags="{{ item.tags | join: ', ' | escape }}"
296
+ data-date="{{ item.lastmod | default: item.date | date: '%Y-%m-%d' }}"
297
+ data-url="{{ item.url | relative_url }}">
298
+
299
+ <td>
300
+ <span class="badge bg-info rounded-pill">{{ collection.label | capitalize }}</span>
301
+ </td>
302
+ <td>
303
+ <a href="{{ item.url | relative_url }}"
304
+ class="text-decoration-none fw-medium">
305
+ {{ item.title | truncate: 50 }}
306
+ </a>
307
+ {% if item.description %}
308
+ <br><small class="text-muted d-md-none">{{ item.description | truncate: 80 }}</small>
309
+ {% endif %}
310
+ </td>
311
+ <td class="d-none d-md-table-cell">
312
+ <small class="text-muted">
313
+ {{ item.description | default: item.excerpt | strip_html | default: "No description available" | truncate: 120 }}
314
+ </small>
315
+ </td>
316
+ <td>
317
+ <small class="text-muted">
318
+ {{ item.lastmod | default: item.date | date: "%b %d, %Y" | default: "Unknown" }}
319
+ </small>
320
+ </td>
321
+ <td class="d-none d-lg-table-cell">
322
+ {% if item.categories and item.categories.size > 0 %}
323
+ {% for category in item.categories limit: 2 %}
324
+ <span class="badge bg-secondary me-1">{{ category }}</span>
325
+ {% endfor %}
326
+ {% if item.categories.size > 2 %}
327
+ <span class="badge bg-light text-dark">+{{ item.categories.size | minus: 2 }}</span>
328
+ {% endif %}
329
+ {% endif %}
330
+ </td>
331
+ <td class="d-none d-xl-table-cell">
332
+ {% if item.tags and item.tags.size > 0 %}
333
+ {% for tag in item.tags limit: 3 %}
334
+ <span class="badge bg-outline-primary me-1">#{{ tag }}</span>
335
+ {% endfor %}
336
+ {% if item.tags.size > 3 %}
337
+ <span class="badge bg-light text-dark">+{{ item.tags.size | minus: 3 }}</span>
338
+ {% endif %}
339
+ {% endif %}
340
+ </td>
341
+ <td class="text-center">
342
+ <div class="btn-group btn-group-sm" role="group">
343
+ <a href="{{ item.url | relative_url }}"
344
+ class="btn btn-outline-primary btn-sm"
345
+ title="View page">
346
+ <i class="bi bi-eye"></i>
347
+ </a>
348
+ <button class="btn btn-outline-secondary btn-sm copy-url"
349
+ data-url="{{ site.url }}{{ item.url }}"
350
+ title="Copy URL">
351
+ <i class="bi bi-clipboard"></i>
352
+ </button>
353
+ </div>
354
+ </td>
355
+ </tr>
356
+ {% endunless %}
357
+ {% endfor %}
358
+ {% endfor %}
359
+ </tbody>
360
+ </table>
361
+ </div>
362
+ </div>
363
+ </div>
364
+ </div>
365
+ </div>
54
366
 
55
- <script>
56
- document.querySelectorAll('th').forEach(header => {
57
- header.addEventListener('click', () => {
58
- const tableElement = document.querySelector('table');
59
- const headerIndex = Array.prototype.indexOf.call(header.parentNode.children, header);
60
- const currentTbody = tableElement.querySelector('tbody');
61
- const newTbody = currentTbody.cloneNode(true);
62
- const rows = Array.from(newTbody.querySelectorAll('tr'));
63
- const type = header.getAttribute('data-type');
64
- const order = header.getAttribute('data-order');
65
- const sortedRows = rows.sort((a, b) => {
66
- const aValue = a.children[headerIndex].textContent;
67
- const bValue = b.children[headerIndex].textContent;
68
- if (type === 'date') {
69
- const aDate = aValue === "null" ? new Date(0) : new Date(...aValue.split('/').map((val, idx) => parseInt(val) - (idx === 1 ? 1 : 0))); // subtract 1 from month
70
- const bDate = bValue === "null" ? new Date(0) : new Date(...bValue.split('/').map((val, idx) => parseInt(val) - (idx === 1 ? 1 : 0))); // subtract 1 from month
71
- return order === 'asc' ? aDate - bDate : bDate - aDate;
72
- }
73
- return order === 'asc' ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue);
74
- });
75
- sortedRows.forEach(row => newTbody.appendChild(row));
76
- tableElement.replaceChild(newTbody, currentTbody);
77
- header.setAttribute('data-order', order === 'asc' ? 'desc' : 'asc');
78
- document.querySelectorAll('.sort-icon').forEach(icon => icon.className = 'sort-icon'); // clear all icons
79
- header.querySelector('.sort-icon').className = order === 'asc' ? 'sort-icon bi bi-arrow-down' : 'sort-icon bi bi-arrow-up'; // set the icon for the sorted column
367
+ <!-- ================================ -->
368
+ <!-- CARDS VIEW CONTAINER -->
369
+ <!-- ================================ -->
370
+ <div class="row d-none" id="cardViewContainer">
371
+ <div class="col-12">
372
+ <div id="sitemapCardsContainer" class="row row-cols-1 row-cols-md-2 row-cols-lg-3 row-cols-xl-4 g-4">
373
+ <!-- Cards will be dynamically generated here -->
374
+ </div>
375
+ </div>
376
+ </div>
80
377
 
81
- });
82
- });
83
- </script>
378
+ <!-- ================================ -->
379
+ <!-- ENHANCED SITEMAP FUNCTIONALITY -->
380
+ <!-- ================================ -->
381
+ <style>
382
+ /* Custom Sitemap Styles */
383
+ .sitemap-col-collection { min-width: 120px; }
384
+ .sitemap-col-title { min-width: 200px; }
385
+ .sitemap-col-date { min-width: 120px; }
386
+ .sitemap-col-actions { width: 80px; }
387
+
388
+ .sort-icon {
389
+ opacity: 0.5;
390
+ transition: opacity 0.2s ease;
391
+ }
392
+
393
+ th:hover .sort-icon {
394
+ opacity: 1;
395
+ }
396
+
397
+ .sort-icon.active {
398
+ opacity: 1;
399
+ color: #0d6efd;
400
+ }
401
+
402
+ #sitemapTable tbody tr {
403
+ transition: background-color 0.15s ease;
404
+ }
405
+
406
+ .badge.bg-outline-primary {
407
+ color: #0d6efd;
408
+ border: 1px solid #0d6efd;
409
+ background-color: transparent;
410
+ }
411
+
412
+ /* Cards view styles */
413
+ .sitemap-card {
414
+ transition: transform 0.2s ease;
415
+ }
416
+
417
+ .sitemap-card:hover {
418
+ transform: translateY(-2px);
419
+ }
420
+
421
+ .sitemap-card .card {
422
+ border: 1px solid var(--bs-border-color);
423
+ transition: box-shadow 0.15s ease-in-out;
424
+ }
425
+
426
+ .sitemap-card .card:hover {
427
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
428
+ }
429
+
430
+ .sitemap-card .card-title a {
431
+ color: inherit;
432
+ transition: color 0.15s ease;
433
+ }
434
+
435
+ .sitemap-card .card-title a:hover {
436
+ color: var(--bs-primary);
437
+ }
84
438
 
85
- <!-- searchbar script -->
439
+ /* Responsive adjustments */
440
+ @media (max-width: 768px) {
441
+ .sitemap-col-collection,
442
+ .sitemap-col-title,
443
+ .sitemap-col-date {
444
+ min-width: auto;
445
+ }
446
+
447
+ .sitemap-card .card-body {
448
+ padding: 0.75rem;
449
+ }
450
+
451
+ .sitemap-card .card-title {
452
+ font-size: 0.9rem;
453
+ }
454
+ }
455
+
456
+ /* Dark mode improvements */
457
+ @media (prefers-color-scheme: dark) {
458
+ .sitemap-card .card {
459
+ border-color: var(--bs-border-color-translucent);
460
+ }
461
+
462
+ .badge.bg-light {
463
+ background-color: var(--bs-gray-600) !important;
464
+ color: var(--bs-gray-100) !important;
465
+ }
466
+ }
467
+ </style>
86
468
 
87
469
  <script>
88
- function searchFunction() {
89
- var input, filter, table, tr, td, i, j, txtValue;
90
- input = document.getElementById("searchBar");
91
- filter = input.value.toLowerCase();
92
- table = document.getElementsByTagName("table");
93
- tr = table[0].getElementsByTagName("tr");
94
-
95
- for (i = 0; i < tr.length; i++) {
96
- td = tr[i].getElementsByTagName("td");
97
- for (j = 0; j < td.length; j++) {
98
- if (td[j]) {
99
- txtValue = td[j].textContent || td[j].innerText;
100
- if (txtValue.toLowerCase().indexOf(filter) > -1) {
101
- tr[i].style.display = "";
102
- break;
103
- } else {
104
- tr[i].style.display = "none";
470
+ /**
471
+ * Enhanced Sitemap Functionality
472
+ * Features: Advanced search, sorting, filtering, statistics, and card/table views
473
+ */
474
+ class SitemapManager {
475
+ constructor() {
476
+ this.searchInput = document.getElementById('searchBar');
477
+ this.collectionFilter = document.getElementById('collectionFilter');
478
+ this.dateFilter = document.getElementById('dateFilter');
479
+ this.clearSearchBtn = document.getElementById('clearSearch');
480
+ this.resetFiltersBtn = document.getElementById('resetFilters');
481
+ this.toggleStatsBtn = document.getElementById('toggleStats');
482
+ this.statsSection = document.getElementById('statsSection');
483
+ this.table = document.getElementById('sitemapTable');
484
+ this.tbody = document.getElementById('sitemapTableBody');
485
+ this.tableViewContainer = document.getElementById('tableViewContainer');
486
+ this.cardViewContainer = document.getElementById('cardViewContainer');
487
+ this.cardsContainer = document.getElementById('sitemapCardsContainer');
488
+ this.tableViewBtn = document.getElementById('tableView');
489
+ this.cardViewBtn = document.getElementById('cardView');
490
+
491
+ this.rows = Array.from(this.tbody.querySelectorAll('tr'));
492
+ this.visibleCount = document.getElementById('visibleCount');
493
+ this.totalCount = document.getElementById('totalCount');
494
+
495
+ this.searchTimeout = null;
496
+ this.collections = new Set();
497
+ this.currentView = 'table';
498
+
499
+ this.init();
500
+ }
501
+
502
+ init() {
503
+ this.setupCollectionFilter();
504
+ this.updateCounts();
505
+ this.updateStatistics();
506
+ this.generateCards();
507
+ this.bindEvents();
508
+ this.handleUrlParams();
509
+ }
510
+
511
+ setupCollectionFilter() {
512
+ // Collect unique collections
513
+ this.rows.forEach(row => {
514
+ const collection = row.dataset.collection;
515
+ if (collection) {
516
+ this.collections.add(collection);
517
+ }
518
+ });
519
+
520
+ // Populate collection filter
521
+ Array.from(this.collections).sort().forEach(collection => {
522
+ const option = document.createElement('option');
523
+ option.value = collection;
524
+ option.textContent = collection.charAt(0).toUpperCase() + collection.slice(1);
525
+ this.collectionFilter.appendChild(option);
526
+ });
527
+ }
528
+
529
+ generateCards() {
530
+ this.cardsContainer.innerHTML = '';
531
+
532
+ this.rows.forEach(row => {
533
+ const collection = row.dataset.collection;
534
+ const title = row.dataset.title;
535
+ const description = row.dataset.description || 'No description available';
536
+ const categories = row.dataset.categories;
537
+ const tags = row.dataset.tags;
538
+ const date = row.dataset.date;
539
+ const url = row.dataset.url;
540
+
541
+ const formattedDate = date ? new Date(date).toLocaleDateString('en-US', {
542
+ year: 'numeric',
543
+ month: 'short',
544
+ day: 'numeric'
545
+ }) : 'Unknown';
546
+
547
+ const badgeClass = collection === 'pages' ? 'bg-primary' : 'bg-info';
548
+
549
+ const cardHtml = `
550
+ <div class="col sitemap-card"
551
+ data-collection="${collection}"
552
+ data-title="${title}"
553
+ data-description="${description}"
554
+ data-categories="${categories}"
555
+ data-tags="${tags}"
556
+ data-date="${date}">
557
+ <div class="card h-100 shadow-sm">
558
+ <div class="card-header d-flex justify-content-between align-items-center">
559
+ <span class="badge ${badgeClass} rounded-pill">${collection.charAt(0).toUpperCase() + collection.slice(1)}</span>
560
+ <small class="text-muted">${formattedDate}</small>
561
+ </div>
562
+ <div class="card-body">
563
+ <h6 class="card-title">
564
+ <a href="${url}" class="text-decoration-none">${title}</a>
565
+ </h6>
566
+ <p class="card-text text-muted small">${description.length > 100 ? description.substring(0, 100) + '...' : description}</p>
567
+ </div>
568
+ <div class="card-footer bg-transparent">
569
+ <div class="d-flex justify-content-between align-items-center">
570
+ <div class="btn-group btn-group-sm" role="group">
571
+ <a href="${url}" class="btn btn-outline-primary btn-sm" title="View page">
572
+ <i class="bi bi-eye"></i>
573
+ </a>
574
+ <button class="btn btn-outline-secondary btn-sm copy-url"
575
+ data-url="${location.origin}${url}" title="Copy URL">
576
+ <i class="bi bi-clipboard"></i>
577
+ </button>
578
+ </div>
579
+ ${categories ? `<small class="text-muted">${categories.split(',').slice(0,2).join(', ')}</small>` : ''}
580
+ </div>
581
+ </div>
582
+ </div>
583
+ </div>
584
+ `;
585
+
586
+ this.cardsContainer.insertAdjacentHTML('beforeend', cardHtml);
587
+ });
588
+
589
+ // Re-attach copy URL event listeners for cards
590
+ this.cardsContainer.querySelectorAll('.copy-url').forEach(btn => {
591
+ btn.addEventListener('click', (e) => this.copyUrl(e));
592
+ });
593
+ }
594
+
595
+ bindEvents() {
596
+ // Search with debouncing
597
+ this.searchInput.addEventListener('input', () => {
598
+ clearTimeout(this.searchTimeout);
599
+ this.searchTimeout = setTimeout(() => this.performSearch(), 300);
600
+ });
601
+
602
+ this.clearSearchBtn.addEventListener('click', () => {
603
+ this.searchInput.value = '';
604
+ this.performSearch();
605
+ this.searchInput.focus();
606
+ });
607
+
608
+ // Filters
609
+ this.collectionFilter.addEventListener('change', () => this.applyFilters());
610
+ this.dateFilter.addEventListener('change', () => this.applyFilters());
611
+
612
+ // Reset filters
613
+ this.resetFiltersBtn.addEventListener('click', () => this.resetFilters());
614
+
615
+ // Toggle statistics
616
+ this.toggleStatsBtn.addEventListener('click', () => this.toggleStatistics());
617
+
618
+ // View mode toggle
619
+ this.tableViewBtn.addEventListener('change', () => {
620
+ if (this.tableViewBtn.checked) {
621
+ this.switchToTableView();
622
+ }
623
+ });
624
+
625
+ this.cardViewBtn.addEventListener('change', () => {
626
+ if (this.cardViewBtn.checked) {
627
+ this.switchToCardView();
628
+ }
629
+ });
630
+
631
+ // Column sorting (table view only)
632
+ this.table.querySelectorAll('th[data-type]').forEach(header => {
633
+ header.addEventListener('click', () => this.sortTable(header));
634
+ });
635
+
636
+ // Copy URL functionality
637
+ document.querySelectorAll('.copy-url').forEach(btn => {
638
+ btn.addEventListener('click', (e) => this.copyUrl(e));
639
+ });
640
+
641
+ // Keyboard shortcuts
642
+ document.addEventListener('keydown', (e) => this.handleKeyboardShortcuts(e));
643
+ }
644
+
645
+ switchToTableView() {
646
+ this.currentView = 'table';
647
+ this.tableViewContainer.classList.remove('d-none');
648
+ this.cardViewContainer.classList.add('d-none');
649
+ }
650
+
651
+ switchToCardView() {
652
+ this.currentView = 'card';
653
+ this.tableViewContainer.classList.add('d-none');
654
+ this.cardViewContainer.classList.remove('d-none');
655
+ }
656
+
657
+ performSearch() {
658
+ const searchTerm = this.searchInput.value.toLowerCase().trim();
659
+
660
+ if (this.currentView === 'table') {
661
+ this.rows.forEach(row => {
662
+ if (!searchTerm) {
663
+ row.style.display = '';
664
+ return;
665
+ }
666
+
667
+ const searchableText = [
668
+ row.dataset.title,
669
+ row.dataset.description,
670
+ row.dataset.categories,
671
+ row.dataset.tags,
672
+ row.dataset.collection
673
+ ].join(' ').toLowerCase();
674
+
675
+ row.style.display = searchableText.includes(searchTerm) ? '' : 'none';
676
+ });
677
+ } else {
678
+ // Card view search
679
+ const cards = this.cardsContainer.querySelectorAll('.sitemap-card');
680
+ cards.forEach(card => {
681
+ if (!searchTerm) {
682
+ card.style.display = '';
683
+ return;
684
+ }
685
+
686
+ const searchableText = [
687
+ card.dataset.title,
688
+ card.dataset.description,
689
+ card.dataset.categories,
690
+ card.dataset.tags,
691
+ card.dataset.collection
692
+ ].join(' ').toLowerCase();
693
+
694
+ card.style.display = searchableText.includes(searchTerm) ? '' : 'none';
695
+ });
696
+ }
697
+
698
+ this.applyFilters();
699
+ }
700
+
701
+ applyFilters() {
702
+ const collectionFilter = this.collectionFilter.value;
703
+ const dateFilter = this.dateFilter.value;
704
+
705
+ if (this.currentView === 'table') {
706
+ this.rows.forEach(row => {
707
+ let show = true;
708
+
709
+ // Skip if already hidden by search
710
+ if (row.style.display === 'none') return;
711
+
712
+ // Collection filter
713
+ if (collectionFilter && row.dataset.collection !== collectionFilter) {
714
+ show = false;
715
+ }
716
+
717
+ // Date filter
718
+ if (dateFilter && show) {
719
+ const rowDate = new Date(row.dataset.date);
720
+ const now = new Date();
721
+
722
+ switch (dateFilter) {
723
+ case 'today':
724
+ show = this.isSameDay(rowDate, now);
725
+ break;
726
+ case 'week':
727
+ show = this.isWithinDays(rowDate, 7);
728
+ break;
729
+ case 'month':
730
+ show = this.isWithinDays(rowDate, 30);
731
+ break;
732
+ case 'year':
733
+ show = this.isWithinDays(rowDate, 365);
734
+ break;
735
+ }
736
+ }
737
+
738
+ row.style.display = show ? '' : 'none';
739
+ });
740
+ } else {
741
+ // Card view filtering
742
+ const cards = this.cardsContainer.querySelectorAll('.sitemap-card');
743
+ cards.forEach(card => {
744
+ let show = true;
745
+
746
+ // Skip if already hidden by search
747
+ if (card.style.display === 'none') return;
748
+
749
+ // Collection filter
750
+ if (collectionFilter && card.dataset.collection !== collectionFilter) {
751
+ show = false;
752
+ }
753
+
754
+ // Date filter
755
+ if (dateFilter && show) {
756
+ const cardDate = new Date(card.dataset.date);
757
+ const now = new Date();
758
+
759
+ switch (dateFilter) {
760
+ case 'today':
761
+ show = this.isSameDay(cardDate, now);
762
+ break;
763
+ case 'week':
764
+ show = this.isWithinDays(cardDate, 7);
765
+ break;
766
+ case 'month':
767
+ show = this.isWithinDays(cardDate, 30);
768
+ break;
769
+ case 'year':
770
+ show = this.isWithinDays(cardDate, 365);
771
+ break;
772
+ }
105
773
  }
774
+
775
+ card.style.display = show ? '' : 'none';
776
+ });
777
+ }
778
+
779
+ this.updateCounts();
780
+ }
781
+
782
+ sortTable(header) {
783
+ const headerIndex = Array.from(header.parentNode.children).indexOf(header);
784
+ const type = header.getAttribute('data-type');
785
+ const currentOrder = header.getAttribute('data-order');
786
+ const newOrder = currentOrder === 'asc' ? 'desc' : 'asc';
787
+
788
+ // Clear all sort icons
789
+ this.table.querySelectorAll('.sort-icon').forEach(icon => {
790
+ icon.className = 'sort-icon bi bi-arrow-down-up ms-1';
791
+ icon.classList.remove('active');
792
+ });
793
+
794
+ // Update current header
795
+ header.setAttribute('data-order', newOrder);
796
+ const icon = header.querySelector('.sort-icon');
797
+ icon.className = `sort-icon bi bi-arrow-${newOrder === 'asc' ? 'up' : 'down'} ms-1 active`;
798
+
799
+ // Sort rows
800
+ const sortedRows = this.rows.sort((a, b) => {
801
+ let aValue = a.children[headerIndex].textContent.trim();
802
+ let bValue = b.children[headerIndex].textContent.trim();
803
+
804
+ if (type === 'date') {
805
+ aValue = aValue === "Unknown" ? new Date(0) : new Date(aValue);
806
+ bValue = bValue === "Unknown" ? new Date(0) : new Date(bValue);
807
+ return newOrder === 'asc' ? aValue - bValue : bValue - aValue;
106
808
  }
809
+
810
+ return newOrder === 'asc' ?
811
+ aValue.localeCompare(bValue) :
812
+ bValue.localeCompare(aValue);
813
+ });
814
+
815
+ // Reorder DOM
816
+ sortedRows.forEach(row => this.tbody.appendChild(row));
817
+ }
818
+
819
+ updateCounts() {
820
+ let visibleCount = 0;
821
+
822
+ if (this.currentView === 'table') {
823
+ visibleCount = this.rows.filter(row => row.style.display !== 'none').length;
824
+ } else {
825
+ const cards = this.cardsContainer.querySelectorAll('.sitemap-card');
826
+ visibleCount = Array.from(cards).filter(card => card.style.display !== 'none').length;
827
+ }
828
+
829
+ this.visibleCount.textContent = visibleCount;
830
+ this.totalCount.textContent = this.rows.length;
831
+
832
+ // Update page statistics
833
+ document.getElementById('totalPages').textContent = this.rows.length;
834
+ document.getElementById('totalCollections').textContent = this.collections.size;
835
+ }
836
+
837
+ updateStatistics() {
838
+ const statsContainer = document.getElementById('collectionStats');
839
+ const collectionCounts = {};
840
+
841
+ this.rows.forEach(row => {
842
+ const collection = row.dataset.collection;
843
+ collectionCounts[collection] = (collectionCounts[collection] || 0) + 1;
844
+ });
845
+
846
+ const statsHtml = Object.entries(collectionCounts)
847
+ .sort(([,a], [,b]) => b - a)
848
+ .map(([collection, count]) => `
849
+ <div class="col-md-6 col-lg-4 mb-3">
850
+ <div class="card text-center h-100">
851
+ <div class="card-body">
852
+ <h5 class="card-title">${collection.charAt(0).toUpperCase() + collection.slice(1)}</h5>
853
+ <p class="card-text display-6 text-primary">${count}</p>
854
+ <small class="text-muted">${((count / this.rows.length) * 100).toFixed(1)}% of total</small>
855
+ </div>
856
+ </div>
857
+ </div>
858
+ `).join('');
859
+
860
+ statsContainer.innerHTML = statsHtml;
861
+ }
862
+
863
+ toggleStatistics() {
864
+ this.statsSection.classList.toggle('d-none');
865
+ const isVisible = !this.statsSection.classList.contains('d-none');
866
+ this.toggleStatsBtn.innerHTML = `
867
+ <i class="bi bi-${isVisible ? 'eye-slash' : 'bar-chart'}"></i>
868
+ ${isVisible ? 'Hide' : 'Stats'}
869
+ `;
870
+ }
871
+
872
+ resetFilters() {
873
+ this.searchInput.value = '';
874
+ this.collectionFilter.value = '';
875
+ this.dateFilter.value = '';
876
+
877
+ // Reset table rows
878
+ this.rows.forEach(row => row.style.display = '');
879
+
880
+ // Reset cards
881
+ const cards = this.cardsContainer.querySelectorAll('.sitemap-card');
882
+ cards.forEach(card => card.style.display = '');
883
+
884
+ this.updateCounts();
885
+
886
+ // Clear URL parameters
887
+ const url = new URL(window.location);
888
+ url.searchParams.delete('q');
889
+ window.history.replaceState({}, document.title, url);
890
+ }
891
+
892
+ copyUrl(event) {
893
+ const url = event.currentTarget.dataset.url;
894
+ navigator.clipboard.writeText(url).then(() => {
895
+ const btn = event.currentTarget;
896
+ const originalIcon = btn.innerHTML;
897
+ btn.innerHTML = '<i class="bi bi-check"></i>';
898
+ btn.classList.add('btn-success');
899
+ btn.classList.remove('btn-outline-secondary');
900
+
901
+ setTimeout(() => {
902
+ btn.innerHTML = originalIcon;
903
+ btn.classList.remove('btn-success');
904
+ btn.classList.add('btn-outline-secondary');
905
+ }, 1500);
906
+ });
907
+ }
908
+
909
+ handleUrlParams() {
910
+ const urlParams = new URLSearchParams(window.location.search);
911
+ const searchQuery = urlParams.get('q');
912
+
913
+ if (searchQuery) {
914
+ this.searchInput.value = searchQuery;
915
+ this.performSearch();
107
916
  }
108
917
  }
918
+
919
+ handleKeyboardShortcuts(event) {
920
+ // Ctrl/Cmd + K to focus search
921
+ if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
922
+ event.preventDefault();
923
+ this.searchInput.focus();
924
+ }
925
+
926
+ // Escape to clear search
927
+ if (event.key === 'Escape' && document.activeElement === this.searchInput) {
928
+ this.searchInput.value = '';
929
+ this.performSearch();
930
+ }
931
+ }
932
+
933
+ // Utility functions
934
+ isSameDay(date1, date2) {
935
+ return date1.toDateString() === date2.toDateString();
936
+ }
937
+
938
+ isWithinDays(date, days) {
939
+ const now = new Date();
940
+ const diffTime = Math.abs(now - date);
941
+ const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
942
+ return diffDays <= days;
943
+ }
109
944
  }
110
- </script>
111
945
 
112
- <script>
113
- // Parse the URL query parameters
114
- const urlParams = new URLSearchParams(window.location.search);
115
- // Get the 'q' parameter
116
- const searchQuery = urlParams.get('q');
117
- // If a search query was provided, set it as the value of the search bar
118
- if (searchQuery) {
119
- document.getElementById('searchBar').value = searchQuery;
120
- // Call the search function
121
- searchFunction();
122
- }
946
+ // Initialize sitemap when DOM is ready
947
+ document.addEventListener('DOMContentLoaded', () => {
948
+ new SitemapManager();
949
+ });
123
950
  </script>