Skip to content

Commit 7c635ed

Browse files
JohanDevlclaude
andcommitted
feat: enhance export interface with improved filtering and mobile design
- Remove unnecessary search bar from export history interface - Implement server-side filtering for export type and status with URL persistence - Fix Export All button color consistency with other export buttons - Add comprehensive mobile responsive design with improved spacing - Enhance touch-friendly interface with larger tap targets and better spacing - Improve visual hierarchy with increased margins and padding on mobile devices - Add filter state persistence in URL parameters for better UX UI Improvements: - Export cards: Increased spacing and padding for better readability - Mobile layout: Single column design with generous spacing - Tablet layout: Adaptive grid with improved touch targets - Filters: Server-side processing for better performance - Buttons: Consistent primary color scheme across all export actions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 989a625 commit 7c635ed

File tree

3 files changed

+254
-41
lines changed

3 files changed

+254
-41
lines changed

pkg/web/handlers/exports.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,10 @@ func (h *ExportsHandler) prepareExportsData(r *http.Request) *ExportsData {
224224
page := h.getIntParam(r, "page", 1)
225225
limit := h.getIntParam(r, "limit", 10)
226226

227+
// Parse filter parameters
228+
typeFilter := r.URL.Query().Get("type")
229+
statusFilter := r.URL.Query().Get("status")
230+
227231
// Validate parameters
228232
if page < 1 {
229233
page = 1
@@ -234,9 +238,12 @@ func (h *ExportsHandler) prepareExportsData(r *http.Request) *ExportsData {
234238
limit = 100
235239
}
236240

237-
// Scan for existing export files with pagination
241+
// Scan for existing export files with filtering
238242
allExports := h.scanExportFiles()
239-
totalItems := len(allExports)
243+
244+
// Apply filters
245+
filteredExports := h.applyFilters(allExports, typeFilter, statusFilter)
246+
totalItems := len(filteredExports)
240247
totalPages := (totalItems + limit - 1) / limit
241248

242249
if totalPages == 0 {
@@ -255,7 +262,7 @@ func (h *ExportsHandler) prepareExportsData(r *http.Request) *ExportsData {
255262
}
256263

257264
if start < totalItems {
258-
data.Exports = allExports[start:end]
265+
data.Exports = filteredExports[start:end]
259266
} else {
260267
data.Exports = []ExportItem{}
261268
}
@@ -287,6 +294,24 @@ func (h *ExportsHandler) getIntParam(r *http.Request, key string, defaultValue i
287294
return defaultValue
288295
}
289296

297+
func (h *ExportsHandler) applyFilters(exports []ExportItem, typeFilter, statusFilter string) []ExportItem {
298+
if typeFilter == "" && statusFilter == "" {
299+
return exports
300+
}
301+
302+
var filtered []ExportItem
303+
for _, export := range exports {
304+
matchesType := typeFilter == "" || export.Type == typeFilter
305+
matchesStatus := statusFilter == "" || export.Status == statusFilter
306+
307+
if matchesType && matchesStatus {
308+
filtered = append(filtered, export)
309+
}
310+
}
311+
312+
return filtered
313+
}
314+
290315
func (h *ExportsHandler) buildPaginationData(currentPage, totalPages, totalItems, itemsPerPage int) *PaginationData {
291316
pagination := &PaginationData{
292317
CurrentPage: currentPage,

web/static/css/style.css

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,20 @@ body {
520520
gap: 1rem;
521521
margin-bottom: 2rem;
522522
flex-wrap: wrap;
523+
justify-content: space-between;
524+
align-items: center;
525+
}
526+
527+
.controls-left {
528+
display: flex;
529+
gap: 1rem;
530+
flex-wrap: wrap;
531+
}
532+
533+
.controls-right {
534+
display: flex;
535+
align-items: center;
536+
gap: 0.5rem;
523537
}
524538

525539
.search-input,
@@ -1309,3 +1323,175 @@ body {
13091323
page-break-inside: avoid;
13101324
}
13111325
}
1326+
1327+
/* Enhanced Mobile Responsive Design for Exports */
1328+
@media (max-width: 768px) {
1329+
.export-types {
1330+
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
1331+
gap: 1.5rem;
1332+
padding: 0 1rem;
1333+
margin-bottom: 2rem;
1334+
}
1335+
1336+
.export-type-card {
1337+
width: 100%;
1338+
min-height: 170px;
1339+
padding: 1.25rem;
1340+
}
1341+
1342+
.export-type-card h3 {
1343+
font-size: 1rem;
1344+
margin: 0.5rem 0;
1345+
}
1346+
1347+
.export-type-card p {
1348+
font-size: 0.85rem;
1349+
margin-bottom: 1rem;
1350+
}
1351+
1352+
.history-controls {
1353+
flex-direction: column;
1354+
gap: 1.25rem;
1355+
margin-bottom: 2.5rem;
1356+
}
1357+
1358+
.controls-left {
1359+
display: flex;
1360+
flex-direction: column;
1361+
gap: 1rem;
1362+
width: 100%;
1363+
}
1364+
1365+
.controls-right {
1366+
display: flex;
1367+
justify-content: space-between;
1368+
align-items: center;
1369+
width: 100%;
1370+
}
1371+
1372+
.filter-select, .page-size-select {
1373+
width: 100%;
1374+
font-size: 1rem;
1375+
padding: 0.75rem;
1376+
}
1377+
1378+
.page-size-label {
1379+
white-space: nowrap;
1380+
margin-right: 0.5rem;
1381+
}
1382+
1383+
.export-item {
1384+
flex-direction: column;
1385+
gap: 1.25rem;
1386+
margin-bottom: 1.5rem;
1387+
padding: 1.25rem;
1388+
}
1389+
1390+
.export-actions-container {
1391+
width: 100%;
1392+
display: flex;
1393+
flex-wrap: wrap;
1394+
gap: 0.5rem;
1395+
}
1396+
1397+
.btn {
1398+
flex: 1;
1399+
min-width: 120px;
1400+
text-align: center;
1401+
}
1402+
1403+
.pagination {
1404+
flex-wrap: wrap;
1405+
justify-content: center;
1406+
gap: 0.25rem;
1407+
}
1408+
1409+
.pagination-btn {
1410+
min-width: 40px;
1411+
padding: 0.5rem 0.75rem;
1412+
}
1413+
}
1414+
1415+
@media (max-width: 480px) {
1416+
.container {
1417+
padding: 1rem 1rem;
1418+
}
1419+
1420+
.export-actions {
1421+
margin-bottom: 2.5rem;
1422+
}
1423+
1424+
.export-types {
1425+
grid-template-columns: 1fr;
1426+
gap: 1.5rem;
1427+
padding: 0 0.75rem;
1428+
margin-bottom: 2.5rem;
1429+
}
1430+
1431+
.export-type-card {
1432+
width: 100%;
1433+
min-height: 160px;
1434+
padding: 1.5rem;
1435+
}
1436+
1437+
.export-icon {
1438+
font-size: 1.5rem;
1439+
}
1440+
1441+
.export-type-card h3 {
1442+
font-size: 0.95rem;
1443+
}
1444+
1445+
.export-type-card p {
1446+
font-size: 0.85rem;
1447+
margin-bottom: 1rem;
1448+
}
1449+
1450+
.btn {
1451+
padding: 0.75rem 1rem;
1452+
font-size: 0.9rem;
1453+
}
1454+
1455+
.controls-right {
1456+
flex-direction: column;
1457+
gap: 1rem;
1458+
align-items: stretch;
1459+
}
1460+
1461+
.page-size-label {
1462+
text-align: center;
1463+
margin-right: 0;
1464+
margin-bottom: 0.5rem;
1465+
}
1466+
1467+
.export-details {
1468+
flex-direction: column;
1469+
gap: 0.5rem;
1470+
align-items: flex-start;
1471+
}
1472+
1473+
.export-details span {
1474+
font-size: 0.85rem;
1475+
}
1476+
1477+
.pagination-btn {
1478+
min-width: 35px;
1479+
padding: 0.4rem 0.6rem;
1480+
font-size: 0.85rem;
1481+
}
1482+
1483+
.pagination-info {
1484+
font-size: 0.85rem;
1485+
text-align: center;
1486+
margin-top: 1rem;
1487+
}
1488+
1489+
.export-actions-container {
1490+
flex-direction: column;
1491+
}
1492+
1493+
.btn {
1494+
width: 100%;
1495+
margin: 0;
1496+
}
1497+
}

web/templates/exports.html

Lines changed: 40 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ <h3>Watchlist</h3>
105105
<div class="export-icon">📦</div>
106106
<h3>Complete Export</h3>
107107
<p>Export all your data</p>
108-
<button class="btn btn-secondary export-btn" data-type="all">
108+
<button class="btn btn-primary export-btn" data-type="all">
109109
Export All
110110
</button>
111111
</div>
@@ -149,12 +149,6 @@ <h2>📋 Export History</h2>
149149

150150
<div class="history-controls">
151151
<div class="controls-left">
152-
<input
153-
type="text"
154-
id="search-exports"
155-
placeholder="Search exports..."
156-
class="search-input"
157-
/>
158152
<select id="filter-type" class="filter-select">
159153
<option value="">All Types</option>
160154
<option value="watched">Watched</option>
@@ -428,11 +422,9 @@ <h3>No exports found</h3>
428422
});
429423

430424
// Add current filters
431-
const searchValue = document.getElementById('search-exports').value;
432425
const typeFilter = document.getElementById('filter-type').value;
433426
const statusFilter = document.getElementById('filter-status').value;
434427

435-
if (searchValue) params.append('search', searchValue);
436428
if (typeFilter) params.append('type', typeFilter);
437429
if (statusFilter) params.append('status', statusFilter);
438430

@@ -469,6 +461,20 @@ <h3>No exports found</h3>
469461
const newUrl = new URL(window.location);
470462
newUrl.searchParams.set('page', currentPage);
471463
newUrl.searchParams.set('limit', currentPageSize);
464+
465+
// Update filter parameters in URL
466+
if (typeFilter) {
467+
newUrl.searchParams.set('type', typeFilter);
468+
} else {
469+
newUrl.searchParams.delete('type');
470+
}
471+
472+
if (statusFilter) {
473+
newUrl.searchParams.set('status', statusFilter);
474+
} else {
475+
newUrl.searchParams.delete('status');
476+
}
477+
472478
window.history.replaceState({}, '', newUrl);
473479
})
474480
.catch(error => {
@@ -739,8 +745,32 @@ <h4>${typeLabel}</h4>
739745
}
740746
}
741747

748+
// Initialize filters from URL parameters
749+
function initializeFiltersFromURL() {
750+
const urlParams = new URLSearchParams(window.location.search);
751+
752+
const typeFilter = urlParams.get('type');
753+
const statusFilter = urlParams.get('status');
754+
755+
if (typeFilter) {
756+
const typeSelect = document.getElementById('filter-type');
757+
if (typeSelect) {
758+
typeSelect.value = typeFilter;
759+
}
760+
}
761+
762+
if (statusFilter) {
763+
const statusSelect = document.getElementById('filter-status');
764+
if (statusSelect) {
765+
statusSelect.value = statusFilter;
766+
}
767+
}
768+
}
769+
742770
// Export button handlers
743771
document.addEventListener("DOMContentLoaded", function () {
772+
// Initialize filters from URL
773+
initializeFiltersFromURL();
744774
document.querySelectorAll(".export-btn").forEach((btn) => {
745775
btn.addEventListener("click", function () {
746776
const type = this.dataset.type;
@@ -772,15 +802,7 @@ <h4>${typeLabel}</h4>
772802
loadPage(1, newPageSize); // Reset to page 1 when changing page size
773803
});
774804

775-
// Search and filter - debounced to avoid too many requests
776-
let searchTimeout;
777-
document.getElementById("search-exports").addEventListener("input", function() {
778-
clearTimeout(searchTimeout);
779-
searchTimeout = setTimeout(() => {
780-
loadPage(1); // Reset to page 1 when searching
781-
}, 500);
782-
});
783-
805+
// Filter handlers
784806
document.getElementById("filter-type").addEventListener("change", function() {
785807
loadPage(1); // Reset to page 1 when filtering
786808
});
@@ -805,26 +827,6 @@ <h4>${typeLabel}</h4>
805827
});
806828
}
807829

808-
function filterExports() {
809-
const search = document
810-
.getElementById("search-exports")
811-
.value.toLowerCase();
812-
const typeFilter = document.getElementById("filter-type").value;
813-
const statusFilter = document.getElementById("filter-status").value;
814-
815-
document.querySelectorAll(".export-item").forEach((item) => {
816-
const type = item.dataset.type;
817-
const status = item.dataset.status;
818-
const text = item.textContent.toLowerCase();
819-
820-
const matchesSearch = !search || text.includes(search);
821-
const matchesType = !typeFilter || type === typeFilter;
822-
const matchesStatus = !statusFilter || status === statusFilter;
823-
824-
item.style.display =
825-
matchesSearch && matchesType && matchesStatus ? "block" : "none";
826-
});
827-
}
828830
</script>
829831
</div>
830832
</main>

0 commit comments

Comments
 (0)