diff --git a/.drone.yml b/.drone.yml index 66389cb..492d4db 100644 --- a/.drone.yml +++ b/.drone.yml @@ -29,3 +29,4 @@ volumes: trigger: branch: - master + - develop \ No newline at end of file diff --git a/instalinks/links/views.py b/instalinks/links/views.py index 775a35a..1036f5b 100644 --- a/instalinks/links/views.py +++ b/instalinks/links/views.py @@ -16,11 +16,27 @@ def links_list(request): """ Return JSON list of all links. """ - if request.method == 'GET': - all_links = Link.objects.all().values('id', 'url', 'watched') - return JsonResponse(list(all_links), safe=False) - else: - return JsonResponse({'error': 'Method not allowed'}, status=405) + + try: + page = int(request.GET.get('page', 1)) + per_page = int(request.GET.get('per_page', 8)) + except ValueError: + return JsonResponse({'error': 'Invalid page or per_page'}, status=400) + + offset = (page - 1) * per_page + limit = offset + per_page + + all_links = Link.objects.all().order_by('-id') # latest first + total = all_links.count() + results = list(all_links[offset:limit].values('id', 'url', 'watched')) + + return JsonResponse({ + 'results': results, + 'total': total, + 'page': page, + 'per_page': per_page, + 'pages': (total + per_page - 1) // per_page + }) @csrf_exempt diff --git a/static/links/main.js b/static/links/main.js index f4cb8f8..e10604b 100644 --- a/static/links/main.js +++ b/static/links/main.js @@ -3,52 +3,65 @@ const addLinkBtn = document.getElementById('addLinkBtn'); const newLinksContainer = document.getElementById('newLinksContainer'); const watchedLinksContainer = document.getElementById('watchedLinksContainer'); +const PER_PAGE = 8; let linksData = []; +let currentPage = 1; -// Fetch all links from the server function loadLinks() { - fetch('/api/links/') + fetch('/api/links/?page=${currentPage}&per_page=${PER_PAGE}') .then(res => res.json()) .then(data => { - linksData = data; // store in a global variable - renderLinks(); + linksData = data.results; + renderPaginatedList(linksData, newLinksContainer, false, data.page, data.pages); + renderPaginatedList(linksData.filter(l => l.watched), watchedLinksContainer, true, data.page, data.pages); }) .catch(err => console.error(err)); } -// Render the links into the two tabs -function renderLinks() { - // separate them - const newLinks = linksData.filter(link => !link.watched); - const watchedLinks = linksData.filter(link => link.watched); +function renderPaginatedList(linkList, container, isWatched, page, totalPages) { + container.innerHTML = ''; - const newRow = document.createElement('div'); - newRow.className = 'row gy-4'; + const row = document.createElement('div'); + row.className = 'row gy-4'; - // Render "New" links - newLinksContainer.innerHTML = ''; - newLinks.forEach(link => { - const col = document.createElement('div'); - col.className = 'col-12 col-sm-6 col-md-4 col-lg-3'; // responsive columns - col.appendChild(createLinkElement(link, false)); - newRow.appendChild(col); - }); + linkList + .filter(link => link.watched === isWatched) + .forEach(link => { + const col = document.createElement('div'); + col.className = 'col-12 col-sm-6 col-md-4 col-lg-3'; + col.appendChild(createLinkElement(link, isWatched)); + row.appendChild(col); + }); - newLinksContainer.appendChild(newRow); + container.appendChild(row); - const watchedRow = document.createElement('div'); - watchedRow.className = 'row gy-4'; + // Pagination controls + if (totalPages > 1) { + const nav = document.createElement('div'); + nav.className = 'd-flex justify-content-center mt-4 gap-2'; - // Render "Watched" links - watchedLinksContainer.innerHTML = ''; - watchedLinks.forEach(link => { - const col = document.createElement('div'); - col.className = 'col-12 col-sm-6 col-md-4 col-lg-3'; - col.appendChild(createLinkElement(link, true)); - watchedRow.appendChild(col); - }); + const prevBtn = document.createElement('button'); + prevBtn.innerText = '← Previous'; + prevBtn.className = 'btn btn-outline-primary'; + prevBtn.disabled = page <= 1; + prevBtn.onclick = () => { + currentPage--; + loadLinks(); + }; - watchedLinksContainer.appendChild(watchedRow); + const nextBtn = document.createElement('button'); + nextBtn.innerText = 'Next →'; + nextBtn.className = 'btn btn-outline-primary'; + nextBtn.disabled = page >= totalPages; + nextBtn.onclick = () => { + currentPage++; + loadLinks(); + }; + + nav.appendChild(prevBtn); + nav.appendChild(nextBtn); + container.appendChild(nav); + } reinitializeInstagramEmbeds(); } @@ -95,7 +108,8 @@ function deleteLink(linkId) { .then(res => { if (res.ok) { linksData = linksData.filter(link => link.id !== linkId); - renderLinks(); + currentPage = 1; + renderPaginatedList(); reinitializeInstagramEmbeds(); } }) @@ -121,7 +135,7 @@ function markLinkWatched(linkId) { if (idx !== -1) { linksData[idx] = updatedLink; } - renderLinks(); + renderPaginatedList(); reinitializeInstagramEmbeds(); }) .catch(err => console.error(err)); @@ -142,7 +156,8 @@ addLinkBtn.addEventListener('click', () => { // update local data linksData.push(addedLink); newLinkInput.value = ''; - renderLinks(); + currentPage = 1; + renderPaginatedList(); reinitializeInstagramEmbeds(); }) .catch(err => console.error(err));