diff --git a/src/pages/CampaignsPage.tsx b/src/pages/CampaignsPage.tsx
index ba98b333..97ea6c6d 100644
--- a/src/pages/CampaignsPage.tsx
+++ b/src/pages/CampaignsPage.tsx
@@ -1,4 +1,5 @@
-import { useMemo } from 'react';
+import { useCallback, useMemo } from 'react';
+import type { ReactNode } from 'react';
import { Link } from 'react-router-dom';
import { useSeoMeta } from '@unhead/react';
import { Trans, useTranslation } from 'react-i18next';
@@ -9,12 +10,14 @@ import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { CampaignCard, CampaignCardSkeleton } from '@/components/CampaignCard';
import { HeroLightningMap } from '@/components/HeroLightningMap';
-import { ModeratorCollapsibleSection, ReorderableCampaignGrid } from '@/components/moderation';
+import { ModeratorCollapsibleSection, ReorderableCampaignGrid, ReorderProvider } from '@/components/moderation';
import { useAuthor } from '@/hooks/useAuthor';
import { useCampaigns } from '@/hooks/useCampaigns';
import { useCampaignModeration } from '@/hooks/useCampaignModeration';
import { useCampaignModerators } from '@/hooks/useCampaignModerators';
import { useCurrentUser } from '@/hooks/useCurrentUser';
+import { useReorderCampaign } from '@/hooks/useReorderCampaign';
+import { useToast } from '@/hooks/useToast';
import { genUserName } from '@/lib/genUserName';
import { useAppContext } from '@/hooks/useAppContext';
import type { ParsedCampaign } from '@/lib/campaign';
@@ -80,6 +83,8 @@ export function CampaignsPage() {
const { t } = useTranslation();
const { config } = useAppContext();
const { user } = useCurrentUser();
+ const reorder = useReorderCampaign();
+ const { toast } = useToast();
const wlcAuthor = useAuthor(WLC_PUBKEY);
const wlcName = wlcAuthor.data?.metadata?.display_name
|| wlcAuthor.data?.metadata?.name
@@ -199,6 +204,51 @@ export function CampaignsPage() {
.sort((a, b) => a.createdAt - b.createdAt);
}, [recentCampaigns, moderation, orderedFeatured]);
+ // Reorder handlers for over-cap featured campaigns that surface in
+ // the chronological "All campaigns" section. They mirror the move
+ // operations on `ReorderableCampaignGrid`, but operate against the
+ // *full* featuredCoords list so a card at position 12 can move to
+ // position 11 (or to the top, promoting it into the visible cap).
+ // No optimistic local reorder here: the chronological grid is
+ // sorted by createdAt, not by featured rank, so a successful move
+ // changes which cards appear in the Verified row above — which
+ // re-renders automatically once the moderation pack invalidates
+ // and refetches. Failure surfaces as a toast, matching
+ // ReorderableCampaignGrid.
+ const onFeaturedMoveToTop = useCallback(
+ async (coord: string) => {
+ try {
+ await reorder.moveToTop(coord, featuredCoords);
+ } catch (err) {
+ const msg = err instanceof Error ? err.message : 'Unknown error';
+ toast({ title: t('moderation.menu.failedReorder'), description: msg, variant: 'destructive' });
+ }
+ },
+ [reorder, featuredCoords, toast, t],
+ );
+ const onFeaturedMoveUp = useCallback(
+ async (coord: string) => {
+ try {
+ await reorder.moveUp(coord, featuredCoords);
+ } catch (err) {
+ const msg = err instanceof Error ? err.message : 'Unknown error';
+ toast({ title: t('moderation.menu.failedReorder'), description: msg, variant: 'destructive' });
+ }
+ },
+ [reorder, featuredCoords, toast, t],
+ );
+ const onFeaturedMoveDown = useCallback(
+ async (coord: string) => {
+ try {
+ await reorder.moveDown(coord, featuredCoords);
+ } catch (err) {
+ const msg = err instanceof Error ? err.message : 'Unknown error';
+ toast({ title: t('moderation.menu.failedReorder'), description: msg, variant: 'destructive' });
+ }
+ },
+ [reorder, featuredCoords, toast, t],
+ );
+
const featuredEmpty =
moderationReady && featuredCoords.length === 0 && !featuredLoading;
@@ -281,24 +331,40 @@ export function CampaignsPage() {
) : allCampaignsChronological.length === 0 ? (