diff --git a/src/app/affiliates/page.tsx b/src/app/affiliates/page.tsx index 03761121..8346591a 100644 --- a/src/app/affiliates/page.tsx +++ b/src/app/affiliates/page.tsx @@ -8,6 +8,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import { Megaphone, Users, TrendingUp, Zap } from "lucide-react"; import { SKILL_CATEGORIES } from "@/lib/constants"; +import { parsePageParam } from "@/lib/pagination"; export const metadata: Metadata = { title: "Affiliate Marketplace | ugig.net", @@ -60,10 +61,10 @@ function commissionUsdHint(offer: { price_sats: number; }, btcUsd: number | null): string | null { if (offer.commission_type === "percentage" && offer.price_sats > 0) { - return `≈ $${(offer.price_sats * offer.commission_rate).toFixed(2)} USD`; + return `≈ $${(offer.price_sats * offer.commission_rate).toFixed(2)} USD`; } if (offer.commission_type === "flat" && offer.commission_flat_sats > 0 && btcUsd) { - return `≈ $${((offer.commission_flat_sats / 1e8) * btcUsd).toFixed(2)} USD`; + return `≈ $${((offer.commission_flat_sats / 1e8) * btcUsd).toFixed(2)} USD`; } return null; } @@ -141,7 +142,7 @@ async function AffiliatesList({ searchParams }: { searchParams: AffiliatesPagePr query = query.order("created_at", { ascending: false }); } - const page = parseInt(queryParams.page || "1"); + const page = parsePageParam(queryParams.page); const limit = 20; const offset = (page - 1) * limit; query = query.range(offset, offset + limit - 1); @@ -240,7 +241,7 @@ async function AffiliatesList({ searchParams }: { searchParams: AffiliatesPagePr )} - {/* product_url domain hint removed — URL is hidden from public listing (#20) */} + {/* product_url domain hint removed — URL is hidden from public listing (#20) */}
- Discover projects built by the community. List yours for 50 ⚡ + Discover projects built by the community. List yours for 50 âš¡ sats.
@@ -337,7 +338,7 @@ export default async function DirectoryPage({ })}`} className="ml-1 hover:text-destructive" > - ✕ + ✕ diff --git a/src/app/for-hire/[[...tags]]/page.tsx b/src/app/for-hire/[[...tags]]/page.tsx index e316b94e..d5bc9fb0 100644 --- a/src/app/for-hire/[[...tags]]/page.tsx +++ b/src/app/for-hire/[[...tags]]/page.tsx @@ -7,6 +7,7 @@ import { GigFiltersWithTags } from "@/components/gigs/GigFiltersWithTags"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { Header } from "@/components/layout/Header"; +import { parsePageParam } from "@/lib/pagination"; import { Briefcase } from "lucide-react"; interface GigsPageProps { @@ -38,7 +39,7 @@ export async function generateMetadata({ params }: GigsPageProps): PromisePeople and agents offering their services. Want to hire instead?{" "} - Post a gig → + Post a gig →
Clients posting work they need done. Looking for work instead?{" "} - Browse "I will..." listings → + Browse "I will..." listings →
- ≈ ${((listing.price_sats / 1e8) * btcUsd).toFixed(2)} + ≈ ${((listing.price_sats / 1e8) * btcUsd).toFixed(2)}
)} @@ -314,7 +315,7 @@ export default async function McpPage({ searchParams }: McpPageProps) {- Browse MCP servers — tools, integrations, and APIs for AI agents. + Browse MCP servers — tools, integrations, and APIs for AI agents.
{/* Filters */} @@ -401,7 +402,7 @@ export default async function McpPage({ searchParams }: McpPageProps) { ...(queryParams.category ? { category: queryParams.category } : {}), ...(queryParams.sort ? { sort: queryParams.sort } : {}), })}`} className="ml-1 hover:text-destructive"> - ✕ + ✕ diff --git a/src/app/prompts/page.tsx b/src/app/prompts/page.tsx index 36d8a6ad..76fedb95 100644 --- a/src/app/prompts/page.tsx +++ b/src/app/prompts/page.tsx @@ -10,18 +10,19 @@ import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import { FileText, Star, Download, Zap } from "lucide-react"; import { PROMPT_CATEGORIES } from "@/lib/constants"; import { CopyLinkButton } from "@/components/ui/CopyLinkButton"; +import { parsePageParam } from "@/lib/pagination"; export const metadata: Metadata = { title: "Prompt Marketplace | ugig.net", description: - "Browse AI prompts — expertly crafted prompts for coding, writing, analysis, creative work, and more.", + "Browse AI prompts — expertly crafted prompts for coding, writing, analysis, creative work, and more.", alternates: { canonical: "/prompts", }, openGraph: { title: "Prompt Marketplace | ugig.net", description: - "Browse AI prompts — expertly crafted prompts for coding, writing, analysis, creative work, and more.", + "Browse AI prompts — expertly crafted prompts for coding, writing, analysis, creative work, and more.", url: "/prompts", type: "website", }, @@ -29,7 +30,7 @@ export const metadata: Metadata = { card: "summary_large_image", title: "Prompt Marketplace | ugig.net", description: - "Browse AI prompts — expertly crafted prompts for coding, writing, analysis, creative work, and more.", + "Browse AI prompts — expertly crafted prompts for coding, writing, analysis, creative work, and more.", }, }; @@ -55,7 +56,7 @@ async function PromptList({ searchParams }: { searchParams: PromptsPageProps["se const queryParams = await searchParams; const [supabase, btcUsd] = await Promise.all([createClient(), fetchBtcRate()]); - const page = parseInt(queryParams.page || "1"); + const page = parsePageParam(queryParams.page); const limit = 21; const offset = (page - 1) * limit; @@ -155,7 +156,7 @@ async function PromptList({ searchParams }: { searchParams: PromptsPageProps["se {btcUsd && (- ≈ ${((listing.price_sats / 1e8) * btcUsd).toFixed(2)} + ≈ ${((listing.price_sats / 1e8) * btcUsd).toFixed(2)}
)} @@ -324,7 +325,7 @@ export default async function PromptsPage({ searchParams }: PromptsPageProps) {- Browse AI prompts — expertly crafted for coding, writing, analysis, and more. + Browse AI prompts — expertly crafted for coding, writing, analysis, and more.
{/* Filters */} @@ -411,7 +412,7 @@ export default async function PromptsPage({ searchParams }: PromptsPageProps) { ...(queryParams.category ? { category: queryParams.category } : {}), ...(queryParams.sort ? { sort: queryParams.sort } : {}), })}`} className="ml-1 hover:text-destructive"> - ✕ + ✕ diff --git a/src/app/skills/page.tsx b/src/app/skills/page.tsx index 34a3bfff..cb9c984d 100644 --- a/src/app/skills/page.tsx +++ b/src/app/skills/page.tsx @@ -9,6 +9,7 @@ import { Skeleton } from "@/components/ui/skeleton"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import { Package, Star, Download, Zap, ShieldCheck, ShieldAlert, ShieldX, Shield } from "lucide-react"; import { SKILL_CATEGORIES, SUPPORTED_AGENT_OPTIONS } from "@/lib/constants"; +import { parsePageParam } from "@/lib/pagination"; export const metadata: Metadata = { title: "AI Agent Skills Marketplace | ugig.net", @@ -54,7 +55,7 @@ async function SkillsList({ searchParams }: { searchParams: SkillsPageProps["sea const queryParams = await searchParams; const [supabase, btcUsd] = await Promise.all([createClient(), fetchBtcRate()]); - const page = parseInt(queryParams.page || "1"); + const page = parsePageParam(queryParams.page); const limit = 21; const offset = (page - 1) * limit; @@ -154,7 +155,7 @@ async function SkillsList({ searchParams }: { searchParams: SkillsPageProps["sea {btcUsd && (- ≈ ${((listing.price_sats / 1e8) * btcUsd).toFixed(2)} + ≈ ${((listing.price_sats / 1e8) * btcUsd).toFixed(2)}
)} @@ -312,7 +313,7 @@ export default async function SkillsPage({ searchParams }: SkillsPageProps) {- Browse agent skills — install tools, automations, and workflows. + Browse agent skills — install tools, automations, and workflows.
{/* Filters */} @@ -390,7 +391,7 @@ export default async function SkillsPage({ searchParams }: SkillsPageProps) { })}`} className="text-xs text-muted-foreground hover:text-destructive flex items-center ml-1" > - ✕ clear + ✕ clear )} @@ -429,7 +430,7 @@ export default async function SkillsPage({ searchParams }: SkillsPageProps) { ...(queryParams.category ? { category: queryParams.category } : {}), ...(queryParams.sort ? { sort: queryParams.sort } : {}), })}`} className="ml-1 hover:text-destructive"> - ✕ + ✕ diff --git a/src/lib/pagination.test.ts b/src/lib/pagination.test.ts new file mode 100644 index 00000000..92fbb8c0 --- /dev/null +++ b/src/lib/pagination.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { parsePageParam } from "./pagination"; + +describe("parsePageParam", () => { + it("defaults missing values to page 1", () => { + expect(parsePageParam(undefined)).toBe(1); + expect(parsePageParam(null)).toBe(1); + expect(parsePageParam("")).toBe(1); + }); + + it("clamps invalid low values to page 1", () => { + expect(parsePageParam("-1")).toBe(1); + expect(parsePageParam("0")).toBe(1); + expect(parsePageParam("abc")).toBe(1); + }); + + it("truncates fractional page values", () => { + expect(parsePageParam("2.9")).toBe(2); + }); + + it("caps very large page values", () => { + expect(parsePageParam("999999999")).toBe(100_000); + }); +}); diff --git a/src/lib/pagination.ts b/src/lib/pagination.ts new file mode 100644 index 00000000..3b162272 --- /dev/null +++ b/src/lib/pagination.ts @@ -0,0 +1,11 @@ +const DEFAULT_MAX_PAGE = 100_000; + +export function parsePageParam( + value: string | null | undefined, + maxPage = DEFAULT_MAX_PAGE +) { + const parsed = parseInt(value || "1", 10); + return Number.isFinite(parsed) + ? Math.min(Math.max(parsed, 1), maxPage) + : 1; +}