Skip to content

Commit

Permalink
feat: Pagination (#3)
Browse files Browse the repository at this point in the history
* feat: Pagination

* feat: pagination & sorting feature

* improvements

---------

Co-authored-by: Rotimi Best <[email protected]>
  • Loading branch information
tunny17 and rotimi-best authored Dec 3, 2024
1 parent 793c1de commit 11eb4a6
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 55 deletions.
2 changes: 1 addition & 1 deletion routes/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { sql } from '../services/db.ts';

const app = new Hono();

const LIMIT_PER_PAGE = 5;
const LIMIT_PER_PAGE = 10;

// Valid sort columns and orders
const VALID_SORT_COLUMNS = ['stars', 'author'] as const;
Expand Down
13 changes: 13 additions & 0 deletions src/lib/functions/debounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeoutId: ReturnType<typeof setTimeout>;

return (...args: Parameters<T>) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func(...args);
}, wait);
};
}
78 changes: 50 additions & 28 deletions src/lib/pages/home/page.svelte
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<script lang="ts">
import { onMount } from 'svelte';
import { API_BASE_URL } from '$lib/config';
import Table from './table.svelte';
import { Skeleton } from '$lib/components/skeleton';
import type { Repository, ApiMetadata } from '$lib/types/repository';
import { debounce } from '$lib/functions/debounce';
import Table from './table.svelte';
let repositories: Repository[] = [];
let apiMetadata: ApiMetadata = {
Expand All @@ -21,39 +21,55 @@
order: 'desc',
},
};
let currentPage: number;
let currentOrder: string;
let isFetching = false;
let isMounted = false;
const fetchRepositories = debounce(
async (pageNumber: number = 1, order: string = 'desc') => {
console.log('fetching repositories');
isFetching = true;
try {
const url = new URL(`${API_BASE_URL}/repositories`);
url.searchParams.set('limit', '10');
url.searchParams.set('page', pageNumber.toString());
url.searchParams.set('order', order);
async function fetchRepositories() {
isFetching = true;
const response = await fetch(url);
try {
console.log('API_BASE_URL', API_BASE_URL);
const response = await fetch(`${API_BASE_URL}/repositories?limit=5`);
if (!response.ok) {
throw new Error('Failed to fetch repositories');
if (!response.ok) {
throw new Error('Failed to fetch repositories');
}
const result = await response.json();
repositories = result.data;
apiMetadata = {
pagination: result.pagination,
sort: result.sort,
};
} catch (err: any) {
console.error(err);
}
const result = await response.json();
repositories = result.data;
apiMetadata = {
pagination: result.pagination,
sort: result.sort,
};
} catch (err: any) {
console.error(err);
}
isFetching = false;
isFetching = false;
}
if (!isMounted) {
isMounted = true;
}
},
1000
);
onMount(() => {
fetchRepositories();
});
$: fetchRepositories(currentPage, currentOrder);
$: gettingFreshData = isFetching && isMounted;
</script>

<div class="mt-5 max-w-2xl mx-auto">
{#if isFetching}
{#if isFetching && !isMounted}
<div class="rounded-md border w-full">
<div class="flex flex-col gap-4 p-4">
<Skeleton class="h-10 w-full" />
Expand All @@ -64,8 +80,6 @@
<Skeleton class="h-8 w-full" />
</div>
</div>

<Skeleton class="h-10 w-full" />
{:else}
<div>
<h2
Expand All @@ -74,6 +88,14 @@
Open source projects in Nigeria
</h2>
</div>
<Table data={repositories} {apiMetadata} />
{#key gettingFreshData}
<Table
data={repositories}
{apiMetadata}
bind:currentPage
bind:currentOrder
isFetching={gettingFreshData}
/>
{/key}
{/if}
</div>
116 changes: 90 additions & 26 deletions src/lib/pages/home/table.svelte
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<script lang="ts">
import { onMount } from 'svelte';
import { readable } from 'svelte/store';
import {
Render,
Expand All @@ -14,17 +15,25 @@
addTableFilter,
} from 'svelte-headless-table/plugins';
import ArrowUpDown from 'lucide-svelte/icons/arrow-up-down';
import ArrowRight from 'lucide-svelte/icons/chevron-right';
import ArrowLeft from 'lucide-svelte/icons/chevron-left';
import { cn } from '$lib/utils';
import { Input } from '$lib/components/input';
import type { Repository, ApiMetadata } from '$lib/types/repository';
import Actions from './table-actions.svelte';
import DataTableCheckbox from './table-checkbox.svelte';
import * as Table from '$lib/components/table/index.js';
import { Button } from '$lib/components/button/index.js';
import { cn } from '$lib/utils';
import { Input } from '$lib/components/input';
import type { Repository, ApiMetadata } from '$lib/types/repository';
export let data: Repository[] = [];
export let apiMetadata: ApiMetadata;
export let currentPage: number;
export let currentOrder: string;
export let isFetching: boolean;
$: console.log('isFetching in table', isFetching);
const table = createTable(readable(data), {
sort: addSortBy({ disableMultiSort: true }),
Expand Down Expand Up @@ -110,22 +119,54 @@
rows,
} = table.createViewModel(columns);
const { sortKeys } = pluginStates.sort;
const { hiddenColumnIds } = pluginStates.hide;
const ids = flatColumns.map((c) => c.id);
let hideForId = Object.fromEntries(ids.map((id) => [id, true]));
const { filterValue } = pluginStates.filter;
const { selectedDataIds } = pluginStates.select;
function previousPage() {
if (currentPage > 1) {
currentPage--;
}
}
function nextPage() {
if (apiMetadata.pagination.hasNextPage) {
currentPage++;
}
}
function onFirstPage() {
currentPage = 1;
}
function onLastPage() {
currentPage = apiMetadata.pagination.totalPages;
}
$: $hiddenColumnIds = Object.entries(hideForId)
.filter(([, hide]) => !hide)
.map(([id]) => id);
const { hasNextPage, hasPreviousPage, pageIndex } = pluginStates.page;
const { filterValue } = pluginStates.filter;
const { selectedDataIds } = pluginStates.select;
$: {
if (typeof currentPage === 'number') {
const url = new URL(window.location.href);
url.searchParams.set('page', currentPage.toString());
history.replaceState(null, '', url.toString());
}
}
console.log('apiMetadata', apiMetadata);
onMount(() => {
const params = new URLSearchParams(window.location.search);
const pageParam = parseInt(params.get('page') || '1', 10);
currentPage =
!isNaN(pageParam) && pageParam > 0
? pageParam
: apiMetadata?.pagination?.currentPage || 1;
currentOrder = apiMetadata?.sort?.order || 'desc';
});
</script>

<div class="w-full">
Expand Down Expand Up @@ -154,18 +195,18 @@
{...attrs}
class={cn('[&:has([role=checkbox])]:pl-3')}
>
{#if cell.id === 'amount'}
<div class="text-right font-medium">
<Render of={cell.render()} />
</div>
{:else if cell.id === 'email'}
<Button variant="ghost" on:click={props.sort.toggle}>
{#if cell.id === 'stars'}
<Button
variant="ghost"
class="pl-0 hover:bg-transparent"
on:click={() =>
(currentOrder =
currentOrder === 'asc' ? 'desc' : 'asc')}
>
<Render of={cell.render()} />
<ArrowUpDown
class={cn(
$sortKeys[0]?.id === cell.id && 'text-foreground',
'ml-2 h-4 w-4'
)}
class="ml-2 h-4 w-4 {currentOrder === 'asc' &&
'text-white'}"
/>
</Button>
{:else}
Expand Down Expand Up @@ -210,19 +251,42 @@
</div>
<div class="flex items-center justify-end space-x-2 py-4">
<div class="text-muted-foreground flex-1 text-sm">
{Object.keys($selectedDataIds).length} of {$rows.length} row(s) selected.
{$rows.length} repo(s) | Page {currentPage} of
{apiMetadata.pagination.totalPages}
</div>

<Button
variant="outline"
size="sm"
on:click={onFirstPage}
disabled={currentPage === 1 || isFetching}
>
First page
</Button>

<Button
variant="outline"
size="sm"
on:click={previousPage}
disabled={!apiMetadata.pagination.hasPrevPage || isFetching}
>
<ArrowLeft />
</Button>
<Button
variant="outline"
size="sm"
on:click={() => ($pageIndex = $pageIndex - 1)}
disabled={!$hasPreviousPage}>Previous</Button
on:click={nextPage}
disabled={!apiMetadata.pagination.hasNextPage || isFetching}
>
<ArrowRight />
</Button>
<Button
variant="outline"
size="sm"
disabled={!$hasNextPage}
on:click={() => ($pageIndex = $pageIndex + 1)}>Next</Button
on:click={onLastPage}
disabled={currentPage === apiMetadata.pagination.totalPages || isFetching}
>
Last page
</Button>
</div>
</div>

0 comments on commit 11eb4a6

Please sign in to comment.