- );
-}
\ No newline at end of file
diff --git a/apps/dashboard/src/components/links/LinkCard.tsx b/apps/dashboard/src/components/links/LinkCard.tsx
index e4ac654..7d63d14 100644
--- a/apps/dashboard/src/components/links/LinkCard.tsx
+++ b/apps/dashboard/src/components/links/LinkCard.tsx
@@ -10,6 +10,7 @@ import {
} from "@useroutr/ui";
import { QrCode, Trash } from "@phosphor-icons/react";
import { motion } from "framer-motion";
+import Link from "next/link";
import { formatCurrency } from "@/lib/utils";
import { LinkStatusBadge } from "./LinkStatusBadge";
import { CopyButton } from "./CopyButton";
@@ -52,12 +53,19 @@ export function LinkCard({ link, onQRCode, onDeactivate }: LinkCardProps) {
>
{/* Header — mono ID + status */}
-
{link.id}
-
+
diff --git a/apps/dashboard/src/hooks/usePaymentLinks.ts b/apps/dashboard/src/hooks/usePaymentLinks.ts
index 00848f9..54dd11b 100644
--- a/apps/dashboard/src/hooks/usePaymentLinks.ts
+++ b/apps/dashboard/src/hooks/usePaymentLinks.ts
@@ -10,14 +10,23 @@ import type {
interface PaymentLinksParams {
page?: number;
limit?: number;
+ /** "all" is normalised away server-side, so it is safe to pass through. */
status?: string;
- search?: string;
}
export function usePaymentLinks(params: PaymentLinksParams = {}) {
return useQuery({
queryKey: ["payment-links", params],
- queryFn: () => api.get("/payment-links", { params: params as Record }),
+ // `/payment-links` answers with `{ data, meta }`. The api client only
+ // unwraps the envelope when there is no `meta`, so both halves arrive here
+ // intact — which is what pagination needs.
+ queryFn: () =>
+ api.get("/payment-links", {
+ params: params as Record,
+ }),
+ // Keeps the previous page on screen while the next one loads, instead of
+ // collapsing the list to a skeleton on every page change.
+ placeholderData: (previous) => previous,
});
}
diff --git a/packages/types/src/payment.types.ts b/packages/types/src/payment.types.ts
index 1ee3275..1621559 100644
--- a/packages/types/src/payment.types.ts
+++ b/packages/types/src/payment.types.ts
@@ -54,11 +54,31 @@ export interface CreatePaymentLinkInput {
expires_at?: string;
}
-export interface PaymentLinksResponse {
- data: PaymentLink[];
- total: number;
+/**
+ * Pagination envelope shared by the paginated list endpoints.
+ *
+ * The API's TransformInterceptor treats a `{ data, meta }` payload as already
+ * wrapped and passes it through untouched, so this is exactly what a client
+ * receives — `meta` is a sibling of `data`, not flattened alongside it.
+ */
+export interface PaginationMeta {
page: number;
limit: number;
+ total: number;
+ totalPages: number;
+}
+
+/**
+ * Response of `GET /v1/payment-links`.
+ *
+ * This previously declared `total`, `page` and `limit` as siblings of `data`,
+ * which is not a shape the API has ever sent. Nothing caught it because the one
+ * consumer read `.data` off it and never touched the counts — so pagination had
+ * nothing to render from.
+ */
+export interface PaymentLinksResponse {
+ data: PaymentLink[];
+ meta: PaginationMeta;
}
export default {};