@@ -517,23 +532,121 @@
// Populate tag filter dropdown
function populateTagFilter() {
const tagFilter = document.getElementById('tagFilter');
- const allTags = [...new Set(allLinks.flatMap(link => link.tags))].sort();
-
+
tagFilter.innerHTML = `
- All Tags (${allTags.length})
- ${allTags.map(tag => {
- const count = allLinks.filter(link => link.tags.includes(tag)).length;
- return `${escapeHtml(tag)} (${count}) `;
+ All Tags
+ ${availableTags.map(tag => {
+ return `${escapeHtml(tag.name)} (${tag.count}) `;
}).join('')}
`;
}
// Filter by specific tag (called when tag is clicked)
- function filterByTag(tag) {
- const tagFilter = document.getElementById('tagFilter');
- tagFilter.value = tag;
- filterAndDisplayLinks();
- showToast(`Filtered by tag: ${tag}`, 'info');
+ function filterByTag(tagName) {
+ const tag = availableTags.find(t => t.name === tagName);
+ if (tag) {
+ const tagFilter = document.getElementById('tagFilter');
+ tagFilter.value = tag.id;
+ filterAndDisplayLinks();
+ showToast(`Filtered by tag: ${tagName}`, 'info');
+ }
+ }
+
+ // Pagination functions
+ function updatePaginationControls() {
+ const paginationContainer = document.getElementById('paginationContainer');
+ const prevButton = document.getElementById('prevPage');
+ const nextButton = document.getElementById('nextPage');
+ const pageNumbers = document.getElementById('pageNumbers');
+ const paginationInfo = document.getElementById('paginationInfo');
+
+ // Show/hide pagination based on whether there are multiple pages
+ if (totalPages > 1) {
+ paginationContainer.classList.remove('hidden');
+ } else {
+ paginationContainer.classList.add('hidden');
+ return;
+ }
+
+ // Update prev/next buttons
+ prevButton.disabled = currentPage === 1;
+ nextButton.disabled = currentPage === totalPages;
+
+ // Generate page numbers
+ const pages = [];
+ const maxVisiblePages = 5;
+ let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
+ let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
+
+ // Adjust start if we're near the end
+ if (endPage - startPage < maxVisiblePages - 1) {
+ startPage = Math.max(1, endPage - maxVisiblePages + 1);
+ }
+
+ // Add first page if not visible
+ if (startPage > 1) {
+ pages.push(1);
+ if (startPage > 2) {
+ pages.push('...');
+ }
+ }
+
+ // Add visible pages
+ for (let i = startPage; i <= endPage; i++) {
+ pages.push(i);
+ }
+
+ // Add last page if not visible
+ if (endPage < totalPages) {
+ if (endPage < totalPages - 1) {
+ pages.push('...');
+ }
+ pages.push(totalPages);
+ }
+
+ pageNumbers.innerHTML = pages.map(page => {
+ if (page === '...') {
+ return `... `;
+ }
+ const isActive = page === currentPage;
+ return `
+
+ ${page}
+
+ `;
+ }).join('');
+
+ // Update pagination info
+ const startItem = (currentPage - 1) * itemsPerPage + 1;
+ const endItem = Math.min(currentPage * itemsPerPage, totalItems);
+ paginationInfo.textContent = `Showing ${startItem}-${endItem} of ${totalItems}`;
+ }
+
+ function goToPage(page) {
+ if (page >= 1 && page <= totalPages && page !== currentPage) {
+ currentPage = page;
+ loadLinks();
+ window.scrollTo({ top: 0, behavior: 'smooth' });
+ }
+ }
+
+ function nextPage() {
+ if (currentPage < totalPages) {
+ goToPage(currentPage + 1);
+ }
+ }
+
+ function prevPage() {
+ if (currentPage > 1) {
+ goToPage(currentPage - 1);
+ }
}
// Clear all filters
@@ -591,12 +704,38 @@
async function loadLinks() {
try {
- const response = await fetch('/api/links');
- const links = await response.json();
+ const searchInput = document.getElementById('searchInput');
+ const tagFilter = document.getElementById('tagFilter');
+ const sortBy = document.getElementById('sortBy');
+ const sortOrder = document.getElementById('sortOrder');
+
+ const searchQuery = searchInput.value || '';
+ const tagId = tagFilter.value || '';
+ const sortField = sortBy.value || 'createdAt';
+ const sortOrderValue = sortOrder.dataset.order === 'desc' ? 'DESC' : 'ASC';
+
+ const params = new URLSearchParams({
+ page: currentPage.toString(),
+ limit: itemsPerPage.toString(),
+ search: searchQuery,
+ sortField: sortField,
+ sortOrder: sortOrderValue
+ });
+
+ if (tagId) {
+ params.append('tagId', tagId);
+ }
+
+ const response = await fetch(`/api/links?${params.toString()}`);
+ const data = await response.json();
- allLinks = links;
- populateTagFilter();
- filterAndDisplayLinks();
+ allLinks = data.links;
+ totalPages = data.totalPages;
+ totalItems = data.total;
+ currentPage = data.page;
+
+ displayLinks();
+ updatePaginationControls();
} catch (error) {
console.error('Error loading links:', error);
@@ -714,6 +853,10 @@ Error loading links
document.getElementById('sortOrder').addEventListener('click', toggleSortOrder);
document.getElementById('clearFilters').addEventListener('click', clearAllFilters);
+ // Pagination controls
+ document.getElementById('prevPage').addEventListener('click', prevPage);
+ document.getElementById('nextPage').addEventListener('click', nextPage);
+
// Tag input functionality
const tagInput = document.getElementById('linkTags');
const debouncedShowSuggestions = debounce((input) => showSuggestions(input), 200);
@@ -740,7 +883,10 @@ Error loading links
// Form and refresh
document.getElementById('addLinkForm').addEventListener('submit', addLink);
- document.getElementById('refreshBtn').addEventListener('click', loadLinks);
+ document.getElementById('refreshBtn').addEventListener('click', () => {
+ currentPage = 1;
+ loadLinks();
+ });
// Initialize
updateSortOrderIcon();
diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt b/app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt
index 216f596..ab22dcc 100644
--- a/app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt
+++ b/app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt
@@ -97,18 +97,38 @@ class LocalServerRepositoryImpl(
get("/api/links") {
try {
+ val page = call.request.queryParameters["page"]?.toIntOrNull() ?: 1
+ val limit = call.request.queryParameters["limit"]?.toIntOrNull() ?: 20
+ val searchQuery = call.request.queryParameters["search"] ?: ""
+ val tagId = call.request.queryParameters["tagId"]?.toLongOrNull()
+ val sortOrder = call.request.queryParameters["sortOrder"] ?: "DESC"
+ val sortField = call.request.queryParameters["sortField"] ?: "createdAt"
+
+ val offset = (page - 1) * limit
+
val links =
deeprQueries
- .getLinksAndTags(
- "",
- "",
- "",
- null,
- "DESC",
- "createdAt",
- "DESC",
- "createdAt",
+ .getLinksAndTagsWithPagination(
+ searchQuery,
+ searchQuery,
+ tagId?.toString() ?: "",
+ tagId,
+ sortOrder,
+ sortField,
+ sortOrder,
+ sortField,
+ limit.toLong(),
+ offset.toLong(),
).executeAsList()
+
+ val total = deeprQueries
+ .countLinksAndTags(
+ searchQuery,
+ searchQuery,
+ tagId?.toString() ?: "",
+ tagId,
+ ).executeAsOne().toInt()
+
val response =
links.map { link ->
LinkResponse(
@@ -120,7 +140,17 @@ class LocalServerRepositoryImpl(
tags = link.tagsNames?.split(", ")?.filter { it.isNotEmpty() } ?: emptyList(),
)
}
- call.respond(HttpStatusCode.OK, response)
+
+ val totalPages = (total + limit - 1) / limit
+ val paginatedResponse = PaginatedLinksResponse(
+ links = response,
+ page = page,
+ limit = limit,
+ total = total,
+ totalPages = totalPages
+ )
+
+ call.respond(HttpStatusCode.OK, paginatedResponse)
} catch (e: Exception) {
Log.e("LocalServer", "Error getting links", e)
call.respond(HttpStatusCode.InternalServerError, ErrorResponse("Error getting links: ${e.message}"))
@@ -265,6 +295,15 @@ data class LinkResponse(
val tags: List,
)
+@Serializable
+data class PaginatedLinksResponse(
+ val links: List,
+ val page: Int,
+ val limit: Int,
+ val total: Int,
+ val totalPages: Int,
+)
+
@Serializable
data class TagData(
val id: Long,
diff --git a/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq b/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq
index 3602e4b..0170bab 100644
--- a/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq
+++ b/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq
@@ -65,6 +65,60 @@ ORDER BY
END
END DESC;
+getLinksAndTagsWithPagination:
+SELECT
+ Deepr.id AS id,
+ Deepr.link,
+ Deepr.name,
+ Deepr.createdAt,
+ Deepr.openedCount,
+ GROUP_CONCAT(Tags.name, ', ') AS tagsNames,
+ GROUP_CONCAT(Tags.id, ', ') AS tagsIds
+FROM
+ Deepr
+ LEFT JOIN LinkTags ON Deepr.id = LinkTags.linkId
+ LEFT JOIN Tags ON LinkTags.tagId = Tags.id
+WHERE
+ (Deepr.link LIKE '%' || ? || '%' OR Deepr.name LIKE '%' || ? || '%')
+ AND (
+ ? = '' OR Tags.id = ?
+ )
+GROUP BY
+ Deepr.id
+ORDER BY
+ CASE WHEN ? = 'ASC' THEN
+ CASE ?
+ WHEN 'createdAt' THEN Deepr.createdAt
+ WHEN 'openedCount' THEN Deepr.openedCount
+ WHEN 'name' THEN Deepr.name
+ WHEN 'link' THEN Deepr.link
+ ELSE Deepr.createdAt
+ END
+ END ASC,
+ CASE WHEN ? = 'DESC' THEN
+ CASE ?
+ WHEN 'createdAt' THEN Deepr.createdAt
+ WHEN 'openedCount' THEN Deepr.openedCount
+ WHEN 'name' THEN Deepr.name
+ WHEN 'link' THEN Deepr.link
+ ELSE Deepr.createdAt
+ END
+ END DESC
+LIMIT ? OFFSET ?;
+
+countLinksAndTags:
+SELECT
+ COUNT(DISTINCT Deepr.id) AS total
+FROM
+ Deepr
+ LEFT JOIN LinkTags ON Deepr.id = LinkTags.linkId
+ LEFT JOIN Tags ON LinkTags.tagId = Tags.id
+WHERE
+ (Deepr.link LIKE '%' || ? || '%' OR Deepr.name LIKE '%' || ? || '%')
+ AND (
+ ? = '' OR Tags.id = ?
+ );
+
listDeeprAsc:
SELECT * FROM Deepr ORDER BY createdAt ASC;