Sponsor Ads System
The sponsor ads system allows directory users to promote their items through paid sponsorships. The system includes a submission workflow, payment integration, admin approval process, and public display of active sponsor ads.
Source Locations
hooks/use-user-sponsor-ads.ts # User-facing CRUD + checkout
hooks/use-admin-sponsor-ads.ts # Admin management (approve/reject/cancel)
hooks/use-active-sponsor-ads.ts # Public display of active ads
hooks/use-sponsor-ad-detail.ts # Single ad detail fetch
lib/types/sponsor-ad.ts # Type definitions
app/api/sponsor-ads/ # API routes
route.ts # GET active ads (public)
checkout/route.ts # POST create checkout
user/route.ts # GET/POST user's ads
user/[id]/route.ts # GET/PUT single ad
user/[id]/cancel/route.ts # POST cancel ad
user/[id]/renew/route.ts # POST renew ad
user/stats/route.ts # GET user stats
Sponsor Ad Lifecycle
User Submits --> pending_payment --> User Pays --> pending --> Admin Reviews
|
+-------+-------+
| |
approved rejected
|
active --> expired
|
cancelled
Status Values
| Status | Description |
|---|---|
pending_payment | Ad created, awaiting payment |
pending | Payment received, awaiting admin approval |
active | Approved and currently displayed |
rejected | Admin rejected the submission |
expired | Active period has ended |
cancelled | Cancelled by user or admin |
Interval Types
| Interval | Duration |
|---|---|
weekly | 7-day sponsorship |
monthly | 30-day sponsorship |
Type Definitions
SponsorAd (Database Schema)
The SponsorAd type comes from the Drizzle schema (lib/db/schema). Key fields include:
id,userId,itemSlug,itemName,itemIconUrl,itemCategorystatus(one of the status values above)interval(weeklyormonthly)startDate,endDatepaymentProvider,paymentId,subscriptionId,customerIdrejectionReason,cancelReasoncreatedAt,updatedAt
SponsorWithItem
Used for display components -- pairs a sponsor ad with its resolved item data:
interface SponsorWithItem {
sponsor: SponsorAd;
item: ItemData | null;
}
SponsorAdStats
Aggregate statistics returned by the stats endpoint:
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;
};
}
useUserSponsorAds
The primary hook for users managing their sponsor ad submissions.
Import
import { useUserSponsorAds } from '@/hooks/use-user-sponsor-ads';
Parameters
interface UseUserSponsorAdsOptions {
page?: number; // default: 1
limit?: number; // default: 10
status?: SponsorAdStatus;
interval?: 'weekly' | 'monthly';
search?: string;
}
Return Value
const {
// Data
sponsorAds, // SponsorAd[]
stats, // SponsorAdStats
// Loading states
isLoading, // boolean - initial fetch
isFetching, // boolean - any fetch including background
isStatsLoading, // boolean - stats query loading
isCreating, // boolean - creation mutation in progress
// Pagination
currentPage, // number
totalPages, // number
totalItems, // number
// Filters
statusFilter, // SponsorAdStatus | undefined
intervalFilter, // 'weekly' | 'monthly' | undefined
search, // string
isSearching, // boolean - debounce in progress
// Actions
createSponsorAd, // (input) => Promise<SponsorAd | null>
cancelSponsorAd, // (id, reason?) => Promise<boolean>
payNow, // (id) => Promise<{ checkoutUrl } | null>
renewSponsorship, // (id) => Promise<{ checkoutUrl } | null>
// Submitting states
isCancelling, // boolean
isPayingNow, // boolean
isRenewing, // boolean
// Filter setters
setStatusFilter, // (status) => void
setIntervalFilter, // (interval) => void
setSearch, // (search) => void
setCurrentPage, // (page) => void
nextPage, // () => void
prevPage, // () => void
// Utility
refreshData, // () => void
} = useUserSponsorAds(options);
Creating a Sponsor Ad
const { createSponsorAd } = useUserSponsorAds();
async function handleSubmit(item) {
const sponsorAd = await createSponsorAd({
itemSlug: item.slug,
itemName: item.name,
itemIconUrl: item.icon,
itemCategory: item.category,
itemDescription: item.description,
interval: 'monthly',
});
if (sponsorAd) {
// Ad created in pending_payment status
// Redirect user to payment
}
}
Payment Flow
After creating a sponsor ad, the user needs to pay. The payNow method creates a checkout session and returns a URL:
const { payNow } = useUserSponsorAds();
async function handlePayment(sponsorAdId: string) {
const result = await payNow(sponsorAdId);
if (result?.checkoutUrl) {
window.location.href = result.checkoutUrl;
}
}
The checkout API (/api/sponsor-ads/checkout) returns:
interface CheckoutResponse {
success: boolean;
data: {
checkoutId: string;
checkoutUrl: string | null;
provider: string;
};
}
Renewing a Sponsorship
Expired or about-to-expire ads can be renewed:
const { renewSponsorship } = useUserSponsorAds();
async function handleRenew(sponsorAdId: string) {
const result = await renewSponsorship(sponsorAdId);
if (result?.checkoutUrl) {
window.location.href = result.checkoutUrl;
}
}