mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-05 11:09:55 -06:00
feat: enhance UI/UX with improved styling and DataTables integration
- Add DataTables library integration for enhanced table functionality - Improve navigation bar styling with rounded corners and white background - Fix mobile button widths for better responsive design - Enhance badge styling with proper text wrapping and ellipsis handling - Add table sorting styles for projects and tasks lists - Improve mobile responsiveness across various screen sizes - Update filter button styling for better visual consistency
This commit is contained in:
@@ -1542,7 +1542,7 @@ main {
|
||||
.navbar-nav.ms-auto { flex: 0 0 auto; }
|
||||
|
||||
/* Allow wrapping to a second row on tighter widths (desktop too) */
|
||||
.navbar-nav { flex-wrap: wrap; row-gap: 0.25rem; column-gap: 0.25rem; }
|
||||
.navbar-nav { flex-wrap: wrap; row-gap: 0.25rem; column-gap: 0.25rem; background-color: white; border-bottom-left-radius: 25px; border-bottom-right-radius: 25px;}
|
||||
.navbar-nav .nav-item { flex: 0 1 auto; min-width: 0; }
|
||||
|
||||
/* body attribute now handled by CSS vars above */
|
||||
|
||||
@@ -568,7 +568,7 @@
|
||||
}
|
||||
|
||||
.mobile-btn {
|
||||
width: 100%;
|
||||
width: auto;
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 1rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
|
||||
@@ -23,6 +23,12 @@
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
||||
<!-- Google Fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
<!-- jQuery (required for DataTables) -->
|
||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
||||
<!-- DataTables CSS and JS -->
|
||||
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.7/css/dataTables.bootstrap5.min.css">
|
||||
<script src="https://cdn.datatables.net/1.13.7/js/jquery.dataTables.min.js"></script>
|
||||
<script src="https://cdn.datatables.net/1.13.7/js/dataTables.bootstrap5.min.js"></script>
|
||||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script>
|
||||
<!-- Custom CSS (cache-busted with app_version) -->
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-filter me-2 text-muted"></i>{{ _('Filter Tasks') }}
|
||||
</h6>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" id="toggleFilters" onclick="toggleFilterVisibility()" title="{{ _('Toggle Filters') }}">
|
||||
<button type="button" class="btn btn-sm" id="toggleFilters" onclick="toggleFilterVisibility()" title="{{ _('Toggle Filters') }}">
|
||||
<i class="fas fa-chevron-up" id="filterToggleIcon"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -327,6 +327,12 @@
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
/* Prevent text wrapping and ensure badge fits content */
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
min-width: fit-content;
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.status-badge::before {
|
||||
@@ -394,7 +400,6 @@
|
||||
|
||||
.priority-badge::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
@@ -772,6 +777,10 @@
|
||||
.status-badge, .priority-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.2rem 0.6rem;
|
||||
/* Ensure badges don't wrap text on medium screens */
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.task-description {
|
||||
@@ -791,6 +800,14 @@
|
||||
.status-badge, .priority-badge {
|
||||
font-size: 0.65rem;
|
||||
padding: 0.15rem 0.5rem;
|
||||
/* Ensure badges don't wrap text on small screens */
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
min-width: fit-content;
|
||||
/* Slightly reduce font size for very long text */
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.task-title-link {
|
||||
|
||||
@@ -346,6 +346,21 @@
|
||||
.filter-toggle-transition {
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
/* Table sorting styles */
|
||||
thead th[style*="cursor: pointer"]:hover {
|
||||
background-color: var(--bs-gray-100);
|
||||
}
|
||||
|
||||
thead th.sorted-asc::after {
|
||||
content: " ▲";
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
|
||||
thead th.sorted-desc::after {
|
||||
content: " ▼";
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="application/json" id="i18n-json-projects-list">
|
||||
@@ -403,31 +418,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
filterBody.classList.add('filter-toggle-transition');
|
||||
}, 100);
|
||||
|
||||
// Initialize DataTable
|
||||
const table = $('#projectsTable').DataTable({
|
||||
order: [[0, 'asc']],
|
||||
pageLength: 25,
|
||||
language: {
|
||||
search: i18nProjects.search_projects || 'Search projects:',
|
||||
lengthMenu: i18nProjects.length_menu || 'Show _MENU_ projects per page',
|
||||
info: i18nProjects.info || 'Showing _START_ to _END_ of _TOTAL_ projects'
|
||||
},
|
||||
responsive: true,
|
||||
dom: '<"row"<"col-sm-12 col-md-6"l><"col-sm-12 col-md-6"f>>' +
|
||||
'<"row"<"col-sm-12"tr>>' +
|
||||
'<"row"<"col-sm-12 col-md-5"i><"col-sm-12 col-md-7"p>>',
|
||||
columnDefs: [
|
||||
{ orderable: false, targets: -1 }
|
||||
]
|
||||
});
|
||||
|
||||
// Custom search
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('keyup', function() {
|
||||
table.search(this.value).draw();
|
||||
});
|
||||
}
|
||||
// Initialize vanilla JavaScript table functionality
|
||||
initializeProjectsTable();
|
||||
|
||||
// Fill progress bars
|
||||
document.querySelectorAll('#projectsTable .progress-bar').forEach(el => {
|
||||
@@ -436,18 +428,109 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
|
||||
function filterByStatus(status) {
|
||||
const table = $('#projectsTable').DataTable();
|
||||
if (status === 'all') {
|
||||
table.column(2).search('').draw();
|
||||
} else {
|
||||
// Match the rendered localized badge text
|
||||
const activeLabel = i18nProjects.status_active || 'Active';
|
||||
const archivedLabel = i18nProjects.status_archived || 'Archived';
|
||||
const label = status === 'active' ? activeLabel : archivedLabel;
|
||||
const regex = `^${label}$`;
|
||||
table.column(2).search(regex, true, false).draw();
|
||||
// Vanilla JavaScript table functionality
|
||||
function initializeProjectsTable() {
|
||||
const table = document.getElementById('projectsTable');
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
|
||||
if (!table) return;
|
||||
|
||||
// Store original rows for filtering
|
||||
const tbody = table.querySelector('tbody');
|
||||
const originalRows = Array.from(tbody.querySelectorAll('tr'));
|
||||
|
||||
// Search functionality
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('keyup', function() {
|
||||
const searchTerm = this.value.toLowerCase();
|
||||
filterTable(searchTerm, 'all');
|
||||
});
|
||||
}
|
||||
|
||||
// Table sorting functionality
|
||||
const headers = table.querySelectorAll('thead th');
|
||||
headers.forEach((header, index) => {
|
||||
// Skip the actions column (last column)
|
||||
if (index === headers.length - 1) return;
|
||||
|
||||
header.style.cursor = 'pointer';
|
||||
header.addEventListener('click', () => sortTable(index));
|
||||
});
|
||||
|
||||
function filterTable(searchTerm, statusFilter) {
|
||||
const rows = tbody.querySelectorAll('tr');
|
||||
|
||||
rows.forEach(row => {
|
||||
const cells = row.querySelectorAll('td');
|
||||
let textMatch = false;
|
||||
let statusMatch = true;
|
||||
|
||||
// Check text match in project name and description
|
||||
if (cells[0]) {
|
||||
const projectText = cells[0].textContent.toLowerCase();
|
||||
textMatch = searchTerm === '' || projectText.includes(searchTerm);
|
||||
}
|
||||
|
||||
// Check status match
|
||||
if (statusFilter !== 'all' && cells[2]) {
|
||||
const statusText = cells[2].textContent.toLowerCase();
|
||||
const activeLabel = (i18nProjects.status_active || 'Active').toLowerCase();
|
||||
const archivedLabel = (i18nProjects.status_archived || 'Archived').toLowerCase();
|
||||
|
||||
if (statusFilter === 'active') {
|
||||
statusMatch = statusText.includes(activeLabel);
|
||||
} else if (statusFilter === 'archived') {
|
||||
statusMatch = statusText.includes(archivedLabel);
|
||||
}
|
||||
}
|
||||
|
||||
row.style.display = (textMatch && statusMatch) ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
function sortTable(columnIndex) {
|
||||
const rows = Array.from(tbody.querySelectorAll('tr'));
|
||||
const isAscending = !table.dataset.sortAsc || table.dataset.sortAsc === 'false';
|
||||
|
||||
rows.sort((a, b) => {
|
||||
const aText = a.cells[columnIndex].textContent.trim();
|
||||
const bText = b.cells[columnIndex].textContent.trim();
|
||||
|
||||
// Try to parse as numbers for numeric columns
|
||||
const aNum = parseFloat(aText.replace(/[^\d.-]/g, ''));
|
||||
const bNum = parseFloat(bText.replace(/[^\d.-]/g, ''));
|
||||
|
||||
if (!isNaN(aNum) && !isNaN(bNum)) {
|
||||
return isAscending ? aNum - bNum : bNum - aNum;
|
||||
} else {
|
||||
return isAscending ? aText.localeCompare(bText) : bText.localeCompare(aText);
|
||||
}
|
||||
});
|
||||
|
||||
// Clear and re-append sorted rows
|
||||
tbody.innerHTML = '';
|
||||
rows.forEach(row => tbody.appendChild(row));
|
||||
|
||||
// Update sort indicator
|
||||
table.dataset.sortAsc = isAscending.toString();
|
||||
|
||||
// Update header indicators
|
||||
headers.forEach(h => h.classList.remove('sorted-asc', 'sorted-desc'));
|
||||
headers[columnIndex].classList.add(isAscending ? 'sorted-asc' : 'sorted-desc');
|
||||
}
|
||||
|
||||
// Store the filter function for external use
|
||||
window.filterProjectsTable = filterTable;
|
||||
}
|
||||
|
||||
function filterByStatus(status) {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const searchTerm = searchInput ? searchInput.value.toLowerCase() : '';
|
||||
|
||||
if (window.filterProjectsTable) {
|
||||
window.filterProjectsTable(searchTerm, status);
|
||||
}
|
||||
|
||||
document.querySelectorAll('.dropdown-item').forEach(i => i.classList.remove('active'));
|
||||
const active = document.querySelector(`[onclick="filterByStatus('${status}')"]`);
|
||||
if (active) active.classList.add('active');
|
||||
|
||||
Reference in New Issue
Block a user