# Paging it yourself

Page a message without a Paginator, using paginate() where the class doesn't fit. Covers the cursor you declare, what paginate() returns, and where the buttons still work.

[`paginate()`](https://docs.seedcord.org/packages/core/latest/paginate) is the slicing math on its own. It takes a list, a page number, and a page size, then returns a [`PageView`](https://docs.seedcord.org/packages/core/latest/page-view) with `totalPages` filled in. The cursor, the buttons, and the routing are all yours to write.

You page by hand when a [`Paginator`](https://docs.seedcord.org/packages/gateway/latest/paginator) can't carry what your message needs, as in these two cases.

* **An embed.** Discord forbids an embed on a ComponentsV2 message, which is what seedcord's reply methods send.
* **Extra values on the button.** A paginator's cursor carries a page and a slot. A filter, a sort key, or a target user id takes a [`CustomId`](https://docs.seedcord.org/packages/custom-id/latest/custom-id) you declare yourself.

```ts title="src/handlers/Leaderboard.ts"
import {
    ButtonHandler,
    ButtonRoute,
    CustomId,
    paginate,
    SlashHandler,
    SlashRoute
} from '@seedcord/gateway';

interface Score {
    name: string;
    points: number;
}

const PER_PAGE = 5;
const PAGE_BOUND = 999;

const Board = new CustomId('lb').int('page', 0, PAGE_BOUND);

function renderBoard(page: number) {
    const view = paginate(SCORES, page, PER_PAGE);
    const row = new BoardRow(
        Board.encode({ page: Math.max(0, view.page - 1) }),
        Board.encode({ page: Math.min(view.page + 1, PAGE_BOUND) }),
        view.hasPrev,
        view.hasNext
    );

    return {
        embeds: [new BoardCard(view).component],
        components: [row.component]
    };
}

@SlashRoute('leaderboard')
export class Leaderboard extends SlashHandler<'leaderboard'> {
    public async execute(): Promise<void> {
        // eslint-disable-next-line @seedcord/no-raw-interaction-acks -- an embed cannot ride a ComponentsV2 reply
        await this.event.reply(renderBoard(0));
    }
}

@ButtonRoute(Board)
export class LeaderboardNav extends ButtonHandler<[typeof Board]> {
    public async execute(): Promise<void> {
        // eslint-disable-next-line @seedcord/no-raw-interaction-acks -- an embed cannot ride a ComponentsV2 reply
        await this.event.update(renderBoard(this.params.page));
    }
}
```

Both handlers call `renderBoard`, so the first reply and every click build the same message from the page number alone.

> **Gateway and http differ**
>
> `this.event` is the discord.js interaction on gateway, which declares `reply` and `update` itself. Http delivers `this.event` as a raw payload that doesn't have ack methods, so the same job goes through [`this.api`](https://docs.seedcord.org/packages/http/latest/repliable-handler#api), whose `api.interactions` callbacks answer Discord themselves.
>
> [Raw acks](/replying/raw-acks) covers both routes and the lint rule the two handlers above disable.

## The cursor you declare

`Board` here carries a page and nothing else. **Prev** targets `page - 1` while **Next** targets `page + 1`, two numbers that never collide.

A third button breaks that. From page 1, **First** and **Prev** both target page 0. Discord rejects a message carrying two identical custom ids, so you give the cursor a second field, `.int('slot', 0, 4)`, and pass each button its own slot.

> **Warning**
>
> `encode` throws `CustomIdValueRejected` once a value passes the bound its field declared. Clamp the target page on both ends before you encode it, the way `Math.min` and `Math.max` do in the fence above.

## What paginate returns

```ts
const view = paginate(scores, 12, 5);
```

Say `scores` holds 20 entries. At 5 per page that's 4 pages, numbered 0 to 3. Asking for page 12 gives back a `view.page` of `3`, which is the fourth and last page. `page` is clamped into `[0, totalPages - 1]`, so a button carrying a stale page number resolves to the nearest real one. A fractional page truncates.

`totalPages` in the hover is a plain `number`, since the whole list was in the argument. An empty list still reports 1.

A `perPage` of zero, a fraction, or a negative throws `PaginationInvalidPerPage`.

## Where the buttons still work

`renderBoard` builds a plain object, so any call taking Discord's message shape accepts it. Both handlers above pass it straight to an ack.

The **Prev** and **Next** buttons route from any message, since the custom id carries everything the handler reads. [`@ButtonRoute`](https://docs.seedcord.org/packages/core/latest/button-route) matches on its prefix alone. On gateway, a weekly job that posts `renderBoard(0)` with `channel.send` gets working buttons too.
