Skip to main content

useUserSponsorAds

Overview

useUserSponsorAds is a comprehensive React hook for managing the current user's sponsor advertisements. It provides paginated listing with filtering and debounced search, aggregate statistics, and mutation actions for creating, cancelling, paying, and renewing sponsor ads. The hook orchestrates multiple TanStack Query queries and mutations with automatic cache invalidation and toast notifications.

Source: template/hooks/use-user-sponsor-ads.ts

Signature

function useUserSponsorAds(options?: UseUserSponsorAdsOptions): UseUserSponsorAdsReturn

Parameters

UseUserSponsorAdsOptions

PropertyTypeDefaultDescription
pagenumber1Initial page number for pagination
limitnumber10Number of items per page
statusSponsorAdStatusundefinedInitial status filter
interval'weekly' | 'monthly'undefinedInitial interval filter
searchstring""Initial search query

SponsorAdStatus

type SponsorAdStatus = 'pending_payment' | 'pending' | 'rejected' | 'active' | 'expired' | 'cancelled';

Return Values

UseUserSponsorAdsReturn

Data

PropertyTypeDescription
sponsorAdsSponsorAd[]Array of sponsor ads for the current page
statsSponsorAdStatsAggregate statistics across all user sponsor ads

SponsorAdStats

interface SponsorAdStats {
overview: {
total: number;
pendingPayment: number;
pending: number;
active: number;
rejected: number;
expired: number;
cancelled: number;
};
byInterval: {
weekly: number;
monthly: number;
};
revenue: {
totalRevenue: number;
weeklyRevenue: number;
monthlyRevenue: number;
};
}

Loading States

PropertyTypeDescription
isLoadingbooleantrue during the initial sponsor ads list fetch
isFetchingbooleantrue whenever the list query is refetching
isStatsLoadingbooleantrue during the initial stats fetch
isCreatingbooleantrue while the create mutation is pending
isCancellingbooleantrue while the cancel mutation is pending
isPayingNowbooleantrue while the pay-now mutation is pending
isRenewingbooleantrue while the renew mutation is pending

Pagination

PropertyTypeDescription
currentPagenumberCurrent page number
totalPagesnumberTotal number of pages
totalItemsnumberTotal number of sponsor ads matching filters

Filters

PropertyTypeDescription
statusFilterSponsorAdStatus | undefinedCurrently active status filter
intervalFilter'weekly' | 'monthly' | undefinedCurrently active interval filter
searchstringCurrent search input value
isSearchingbooleantrue while the search value is being debounced

Actions

MethodSignatureDescription
createSponsorAd(input: CreateSponsorAdInput) => Promise<SponsorAd | null>Create a new sponsor ad. Returns the created record or null on failure.
cancelSponsorAd(id: string, cancelReason?: string) => Promise<boolean>Cancel a sponsor ad. Returns true on success.
payNow(id: string) => Promise<{ checkoutUrl: string } | null>Initiate payment checkout. Returns checkout URL or null.
renewSponsorship(id: string) => Promise<{ checkoutUrl: string } | null>Renew an expired sponsorship. Returns checkout URL or null.
CreateSponsorAdInput
FieldTypeRequiredDescription
itemSlugstringYesSlug of the item to sponsor
itemNamestringYesDisplay name of the item
itemIconUrlstringNoIcon URL for the item
itemCategorystringNoCategory of the item
itemDescriptionstringNoShort description of the item
interval'weekly' | 'monthly'YesBilling interval for the ad

Filter Actions

MethodSignatureDescription
setStatusFilter(status: SponsorAdStatus | undefined) => voidSet or clear the status filter
setIntervalFilter(interval: 'weekly' | 'monthly' | undefined) => voidSet or clear the interval filter
setSearch(search: string) => voidUpdate the search query
setCurrentPage(page: number) => voidJump to a specific page
nextPage() => voidNavigate to the next page
prevPage() => voidNavigate to the previous page

Utility

MethodSignatureDescription
refreshData() => voidInvalidate all user sponsor ads queries to force a fresh fetch

Implementation Details

  • Two independent queries: The hook runs separate queries for the sponsor ads list (/api/sponsor-ads/user) and stats (/api/sponsor-ads/user/stats), allowing them to load and cache independently.
  • Debounced search: The search input is debounced with a 300ms delay via useDebounceValue to prevent excessive API calls.
  • Cache configuration: Both queries use a staleTime of 2 minutes and gcTime of 5 minutes.
  • Automatic cache invalidation: The createSponsorAd and cancelSponsorAd mutations automatically invalidate all queries under the ['user-sponsor-ads'] key family.
  • Toast notifications: Success and error toasts are shown via sonner for create and cancel operations.
  • Checkout flow: payNow and renewSponsorship return checkout URLs from the server. The calling component is responsible for redirecting the user to the checkout page.

Query Keys

const userSponsorAdsQueryKeys = {
all: ['user-sponsor-ads'] as const,
lists: () => [...userSponsorAdsQueryKeys.all, 'list'] as const,
list: (filters: Record<string, unknown>) => [...userSponsorAdsQueryKeys.lists(), filters] as const,
stats: () => [...userSponsorAdsQueryKeys.all, 'stats'] as const,
};

Usage Examples

import { useUserSponsorAds } from '@/hooks/use-user-sponsor-ads';

function SponsorAdsDashboard() {
const {
sponsorAds,
stats,
isLoading,
currentPage,
totalPages,
nextPage,
prevPage,
statusFilter,
setStatusFilter,
search,
setSearch,
} = useUserSponsorAds({ limit: 10 });

if (isLoading) return <div>Loading...</div>;

return (
<div>
<div className="stats-bar">
<span>Active: {stats.overview.active}</span>
<span>Pending: {stats.overview.pending}</span>
<span>Total Revenue: ${stats.revenue.totalRevenue}</span>
</div>

<input
placeholder="Search sponsor ads..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>

<select
value={statusFilter || ''}
onChange={(e) => setStatusFilter(e.target.value as any || undefined)}
>
<option value="">All Statuses</option>
<option value="active">Active</option>
<option value="pending">Pending</option>
<option value="expired">Expired</option>
</select>

<SponsorAdsList ads={sponsorAds} />

<div className="pagination">
<button onClick={prevPage} disabled={currentPage <= 1}>Previous</button>
<span>Page {currentPage} of {totalPages}</span>
<button onClick={nextPage} disabled={currentPage >= totalPages}>Next</button>
</div>
</div>
);
}

Creating a new sponsor ad

import { useUserSponsorAds } from '@/hooks/use-user-sponsor-ads';

function CreateSponsorAdForm({ item }) {
const { createSponsorAd, isCreating } = useUserSponsorAds();

const handleCreate = async () => {
const result = await createSponsorAd({
itemSlug: item.slug,
itemName: item.name,
itemIconUrl: item.icon_url,
itemCategory: item.category,
interval: 'monthly',
});

if (result) {
console.log('Created sponsor ad:', result.id);
}
};

return (
<button onClick={handleCreate} disabled={isCreating}>
{isCreating ? 'Creating...' : 'Sponsor This Item'}
</button>
);
}

Payment and renewal flow

import { useUserSponsorAds } from '@/hooks/use-user-sponsor-ads';

function SponsorAdActions({ ad }) {
const { payNow, renewSponsorship, cancelSponsorAd, isPayingNow, isRenewing } =
useUserSponsorAds();

const handlePay = async () => {
const result = await payNow(ad.id);
if (result) {
window.location.href = result.checkoutUrl;
}
};

const handleRenew = async () => {
const result = await renewSponsorship(ad.id);
if (result) {
window.location.href = result.checkoutUrl;
}
};

const handleCancel = async () => {
await cancelSponsorAd(ad.id, 'No longer needed');
};

return (
<div>
{ad.status === 'pending_payment' && (
<button onClick={handlePay} disabled={isPayingNow}>Pay Now</button>
)}
{ad.status === 'expired' && (
<button onClick={handleRenew} disabled={isRenewing}>Renew</button>
)}
{ad.status === 'active' && (
<button onClick={handleCancel}>Cancel Sponsorship</button>
)}
</div>
);
}