Add trust center updates list and detail pages
Build the public Updates pages in the compliance portal: a cursor-paginated list of sent mailing-list updates and a detail view for a single update, replacing the previous stub page. Add a MailingListUpdate case to the trust API node resolver, guarded so only SENT updates belonging to the current trust center's mailing list are exposed, so the detail page can load an update by URL. Add a Prev/Next Pagination primitive to the v2 UI kit. Page numbers are omitted because cursor pagination cannot derive an ordinal page index; each arrow only shows when its page exists while keeping its slot reserved so a visible arrow never shifts position. Relocate the shared MailingListUpdateListItem to its own component folder and wrap each row in a link to the detail page, so both the home recent-updates section and the list navigate to detail. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
81
packages/ui/src/v2/Pagination/Pagination.tsx
Normal file
81
packages/ui/src/v2/Pagination/Pagination.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { CaretLeftIcon, CaretRightIcon } from "@phosphor-icons/react";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
|
||||
import { Button } from "../Button/Button";
|
||||
import { Text } from "../typography/Text";
|
||||
|
||||
import { pagination } from "./variants";
|
||||
|
||||
export type PaginationProps = Omit<ComponentProps<"nav">, "onChange"> & {
|
||||
hasPrevious: boolean;
|
||||
hasNext: boolean;
|
||||
// Current-position label rendered between the arrows (e.g. "Page 2").
|
||||
label?: ReactNode;
|
||||
// Accessible labels for the arrow controls.
|
||||
previousLabel?: string;
|
||||
nextLabel?: string;
|
||||
onPrevious: () => void;
|
||||
onNext: () => void;
|
||||
};
|
||||
|
||||
// Prev/Next pager for cursor-paginated lists. Page numbers are intentionally
|
||||
// omitted because cursor pagination cannot compute a total page count; an
|
||||
// optional current-position label sits between the arrows instead. Each arrow
|
||||
// only renders when its page exists, but its slot is always reserved (the
|
||||
// missing arrow is kept invisible) so a visible arrow sits in the exact same
|
||||
// position whether or not the other is present. Renders nothing when neither
|
||||
// page exists.
|
||||
export function Pagination(props: PaginationProps) {
|
||||
const {
|
||||
hasPrevious, hasNext, label,
|
||||
previousLabel = "Previous page", nextLabel = "Next page",
|
||||
onPrevious, onNext, className, ...rest
|
||||
} = props;
|
||||
const { root, label: labelSlot } = pagination();
|
||||
|
||||
if (!hasPrevious && !hasNext) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<nav className={root({ className })} {...rest}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
size={2}
|
||||
iconStart={<CaretLeftIcon />}
|
||||
aria-label={previousLabel}
|
||||
className={hasPrevious ? undefined : "invisible"}
|
||||
onClick={onPrevious}
|
||||
/>
|
||||
{label != null && (
|
||||
<Text size={2} color="faint" className={labelSlot()}>
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
color="neutral"
|
||||
size={2}
|
||||
iconStart={<CaretRightIcon />}
|
||||
aria-label={nextLabel}
|
||||
className={hasNext ? undefined : "invisible"}
|
||||
onClick={onNext}
|
||||
/>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
27
packages/ui/src/v2/Pagination/PaginationSkeleton.tsx
Normal file
27
packages/ui/src/v2/Pagination/PaginationSkeleton.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { pagination } from "./variants";
|
||||
|
||||
// Loading placeholder paired with Pagination: two pulse arrow blocks.
|
||||
export function PaginationSkeleton() {
|
||||
const { root, buttonPlaceholder } = pagination();
|
||||
|
||||
return (
|
||||
<div className={root()} aria-hidden>
|
||||
<div className={buttonPlaceholder()} />
|
||||
<div className={buttonPlaceholder()} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
26
packages/ui/src/v2/Pagination/variants.ts
Normal file
26
packages/ui/src/v2/Pagination/variants.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { tv } from "tailwind-variants/lite";
|
||||
|
||||
// Prev/Next pager (Figma "Pagination"). A centered row with a previous button,
|
||||
// a current-position label, and a next button. Slots are shared by the pager
|
||||
// and its skeleton so the loading placeholder matches the real layout.
|
||||
export const pagination = tv({
|
||||
slots: {
|
||||
root: "flex items-center justify-center gap-2",
|
||||
label: "min-w-16 text-center",
|
||||
buttonPlaceholder: "size-8 shrink-0 animate-pulse rounded-2 bg-sand-3",
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user