Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions src/app/claim/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Skeleton } from "@/components/ui/skeleton"
import { Card } from "@/components/ui/card"

export default function ClaimLoading() {
return (
<div className="min-h-screen bg-background relative overflow-hidden">
<div className="relative z-10">
{/* Header skeleton */}
<header className="border-b border-border/50 backdrop-blur-sm">
<div className="container mx-auto px-4 py-4 lg:py-3">
<Skeleton className="h-[75px] w-[150px] rounded" />
</div>
</header>

{/* Hero + content skeleton */}
<main className="container mx-auto px-4 py-12 sm:py-16 lg:py-10">
<div className="max-w-4xl mx-auto text-center space-y-6 sm:space-y-8 lg:space-y-6">
{/* Hero skeleton */}
<div className="space-y-4 sm:space-y-5 lg:space-y-4">
<div className="inline-flex justify-center">
<Skeleton className="h-8 w-32 rounded-full" />
</div>
<div className="space-y-2">
<Skeleton className="mx-auto h-10 w-48 rounded" />
<Skeleton className="mx-auto h-12 w-64 rounded" />
</div>
<Skeleton className="mx-auto h-6 max-w-xl rounded" />
</div>

{/* CTA button skeleton */}
<div className="flex justify-center pt-2 lg:pt-3">
<Skeleton className="h-12 w-40 rounded-md" />
</div>

{/* Feature cards skeleton */}
<div className="grid md:grid-cols-3 gap-6 sm:gap-8 lg:gap-6 pt-12 sm:pt-16 lg:pt-10">
{[1, 2, 3].map((i) => (
<Card key={i} className="p-6 lg:p-5 bg-card/50 border-border/50">
<div className="space-y-3 lg:space-y-2.5">
<Skeleton className="h-11 w-11 rounded-xl" />
<Skeleton className="h-6 w-24 rounded" />
<Skeleton className="h-4 w-full rounded" />
<Skeleton className="h-4 w-full rounded" />
</div>
</Card>
))}
</div>

{/* Stats row skeleton */}
<div className="pt-16 grid grid-cols-2 md:grid-cols-3 gap-8">
{[1, 2, 3].map((i) => (
<div key={i} className="space-y-2">
<Skeleton className="h-9 w-20 rounded" />
<Skeleton className="h-4 w-24 rounded" />
</div>
))}
</div>
</div>
</main>
</div>
</div>
)
}
5 changes: 5 additions & 0 deletions src/app/referral/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { ReferralPageSkeleton } from "@/components/referral/ReferralPageSkeleton"

export default function ReferralLoading() {
return <ReferralPageSkeleton />
}
9 changes: 2 additions & 7 deletions src/app/referral/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ArrowRight } from "lucide-react"
import { Fuul } from "@fuul/sdk"
import { isAddress } from "viem"
import "@/lib/fuul"
import { ReferralPageSkeleton } from "@/components/referral/ReferralPageSkeleton"

function ReferralPageContent() {
const router = useRouter()
Expand Down Expand Up @@ -166,13 +167,7 @@ function ReferralPageContent() {

export default function ReferralPage() {
return (
<Suspense
fallback={
<div className="min-h-screen bg-background flex items-center justify-center">
<div className="animate-pulse text-muted-foreground">Loading...</div>
</div>
}
>
<Suspense fallback={<ReferralPageSkeleton />}>
<ReferralPageContent />
</Suspense>
)
Expand Down
39 changes: 39 additions & 0 deletions src/components/referral/ReferralPageSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Skeleton } from "@/components/ui/skeleton"

/**
* Shared skeleton for the referral page layout.
* Used by src/app/referral/loading.tsx and the Suspense fallback in page.tsx
* so LCP is fast and CLS is minimal when real content loads.
*/
export function ReferralPageSkeleton() {
return (
<div className="min-h-screen bg-background relative overflow-hidden">
{/* Background effects - match referral page */}
<div className="absolute inset-0 bg-[linear-gradient(to_right,hsl(var(--border))_1px,transparent_1px),linear-gradient(to_bottom,hsl(var(--border))_1px,transparent_1px)] bg-[size:4rem_4rem] opacity-10" />
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-primary/10 rounded-full blur-[120px] pointer-events-none" />

<div className="relative z-10 flex flex-col min-h-screen">
{/* Header - logo area + Referral title */}
<header className="border-b border-border/50 backdrop-blur-sm">
<div className="container mx-auto px-4 py-4 lg:py-3 flex items-center justify-between">
<Skeleton className="h-10 w-10 rounded sm:h-[75px] sm:w-[150px]" />
<Skeleton className="h-5 w-16 rounded" />
</div>
</header>

{/* Main - hero + CTA skeleton */}
<main className="flex-1 flex items-center justify-center px-4 py-8">
<div className="w-full max-w-[560px] mx-auto space-y-6">
<div className="text-center space-y-4">
<Skeleton className="mx-auto h-10 w-64 sm:w-80 rounded" />
<Skeleton className="mx-auto h-6 w-72 rounded" />
</div>
<div className="flex justify-center">
<Skeleton className="h-12 w-24 rounded-md" />
</div>
</div>
</main>
</div>
</div>
)
}
86 changes: 32 additions & 54 deletions src/lib/analytics-server.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,24 @@
import { env } from "@/env/server"
import { getLeaderboard } from "@/lib/analytics/services/leaderboard.service"
import { transformLeaderboardRows } from "@/lib/analytics/services/leaderboard-transform"
import { getActiveTraders as getActiveTradersService } from "@/lib/analytics/services/transactions.service"
import {
getActiveTraders as getActiveTradersService,
getCumulativeSuccessfulTransactions as getCumulativeSuccessfulTransactionsService,
getSwapVolume as getSwapVolumeService,
} from "@/lib/analytics/services/transactions.service"

const FUUL_PAYOUTS_SUMMARY_URL = "https://api.fuul.xyz/api/v1/payouts/summary"

/**
* Server-side function to fetch cumulative successful transactions from analytics API
* Calls the internal API route which handles the external API call
* Server-side function to fetch cumulative successful transactions.
* Calls the analytics service directly to avoid self-referencing HTTP on Vercel.
*/
export async function getCumulativeTransactions(): Promise<number | null> {
try {
// Call the internal API route
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"
const response = await fetch(`${baseUrl}/api/analytics/transactions`, {
cache: "no-store",
const cumulativeTxs = await getCumulativeSuccessfulTransactionsService({
catalog: "fastrpc",
})

if (!response.ok) {
console.error("Failed to fetch transactions:", response.statusText)
return null
}

const data = await response.json()

if (data.success && data.cumulativeSuccessfulTxs !== null) {
return Number(data.cumulativeSuccessfulTxs)
}

return null
return cumulativeTxs
} catch (error) {
console.error("Error fetching cumulative transactions:", error)
return null
Expand Down Expand Up @@ -65,30 +57,15 @@ export async function getEthPrice(): Promise<number | null> {
}

/**
* Server-side function to fetch cumulative swap volume from analytics API
* Calls the internal API route which handles the external API call
* Server-side function to fetch cumulative swap volume.
* Calls the analytics service directly to avoid self-referencing HTTP on Vercel.
*/
export async function getCumulativeSwapVolume(): Promise<{
eth: number | null
usd: number | null
}> {
try {
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"
const response = await fetch(`${baseUrl}/api/analytics/volume/swap`, {
cache: "no-store",
})

if (!response.ok) {
console.error("Failed to fetch swap volume:", response.statusText)
return { eth: null, usd: null }
}

const data = await response.json()
if (!data.success) return { eth: null, usd: null }

const eth = data.cumulativeSwapVolEth != null ? Number(data.cumulativeSwapVolEth) : null
const usd = data.cumulativeSwapVolUsd != null ? Number(data.cumulativeSwapVolUsd) : null
return { eth, usd }
return await getSwapVolumeService()
} catch (error) {
console.error("Error fetching cumulative swap volume:", error)
return { eth: null, usd: null }
Expand Down Expand Up @@ -126,33 +103,34 @@ export async function getSwapTransactionCount(): Promise<number | null> {
}

/**
* Server-side function to fetch total points earned from Fuul payout summary
* Calls the internal API route which handles the external API call
* Server-side function to fetch total points earned from Fuul payout summary.
* Calls Fuul API directly to avoid self-referencing HTTP on Vercel.
*/
export async function getTotalPointsEarned(): Promise<number | null> {
try {
// Call the internal API route
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000"
const response = await fetch(`${baseUrl}/api/fuul/payouts-summary?currency=point`, {
const fuulApiKey = env.FUUL_API_KEY
if (!fuulApiKey) {
console.error("FUUL_API_KEY not configured")
return null
}
const url = new URL(FUUL_PAYOUTS_SUMMARY_URL)
url.searchParams.set("currency", "point")
const response = await fetch(url.toString(), {
method: "GET",
headers: {
accept: "application/json",
Authorization: `Bearer ${fuulApiKey}`,
},
cache: "no-store",
})

if (!response.ok) {
console.error("Failed to fetch payout summary:", response.statusText)
console.error("Failed to fetch payout summary:", response.status, await response.text())
return null
}

const data = await response.json()

if (
data.success &&
data.data &&
data.data.total_payouts !== null &&
data.data.total_payouts !== undefined
) {
if (data.data && data.data.total_payouts !== null && data.data.total_payouts !== undefined) {
return Number(data.data.total_payouts)
}

return null
} catch (error) {
console.error("Error fetching total points earned:", error)
Expand Down