Skip to content
Closed
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
240 changes: 191 additions & 49 deletions src/features/trending/components/Invitation/CollectRewardsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, {
useState, useEffect, useCallback, useMemo,
} from 'react';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { AlertCircle } from 'lucide-react';
import { AlertCircle, Sparkles, TrendingUp } from 'lucide-react';
import { useAeSdk } from '@/hooks/useAeSdk';
import { getAffiliationTreasury } from '@/libs/affiliation';
import { Decimal } from '@/libs/decimal';
Expand Down Expand Up @@ -65,6 +65,45 @@ function normalizeAddressToAettosEntries(value: unknown): Array<[string, string]
return [];
}

const REWARD_PARTICLES = [
{
id: 'p0',
left: '20%',
top: '30%',
delay: '0s',
},
{
id: 'p1',
left: '35%',
top: '70%',
delay: '0.3s',
},
{
id: 'p2',
left: '50%',
top: '30%',
delay: '0.6s',
},
{
id: 'p3',
left: '65%',
top: '70%',
delay: '0.9s',
},
{
id: 'p4',
left: '80%',
top: '30%',
delay: '1.2s',
},
{
id: 'p5',
left: '95%',
top: '70%',
delay: '1.5s',
},
] as const;

const CollectRewardsCard = () => {
const { sdk, activeAccount } = useAeSdk();

Expand Down Expand Up @@ -259,73 +298,160 @@ const CollectRewardsCard = () => {
}
if (!thresholdReached) return 'Not eligible yet';
if (accumulatedRewardsAe.lte(Decimal.ZERO)) return 'No rewards yet';
return 'Collect rewards';
return (
<span className="flex items-center justify-center gap-2">
<Sparkles className="w-5 h-5" />
Collect rewards
</span>
);
}, [collectingReward, thresholdReached, accumulatedRewardsAe]);

const collectRewardsShellClassName = [
'bg-[var(--glass-bg)] border border-[var(--glass-border)] backdrop-blur-[20px]',
'rounded-[20px] p-6 md:p-8 lg:p-10 relative overflow-hidden min-h-0',
'transition-all duration-300 hover:border-white/15',
'hover:shadow-[0_8px_32px_rgba(0,0,0,0.3),0_2px_8px_rgba(0,0,0,0.2)] hover:-translate-y-1',
].join(' ');

return (
<div className="bg-black/20 backdrop-blur-lg border border-white/10 rounded-2xl p-6 md:p-8 lg:p-10 relative overflow-hidden min-h-0 before:content-[''] before:absolute before:top-0 before:left-0 before:right-0 before:h-px before:bg-gradient-to-r before:from-pink-400 before:via-purple-400 before:to-blue-400 before:opacity-0 before:transition-opacity before:duration-300 hover:before:opacity-100">
<div className={collectRewardsShellClassName}>
{/* Animated background particles */}
{isEligibleForRewards && (
<div className="absolute inset-0 overflow-hidden pointer-events-none">
{REWARD_PARTICLES.map((p) => (
<div
key={p.id}
className="absolute w-2 h-2 bg-pink-400/30 rounded-full animate-ping"
style={{
left: p.left,
top: p.top,
animationDelay: p.delay,
animationDuration: '2s',
}}
/>
))}
</div>
)}

{/* Header */}
<div className="flex items-center gap-4 mb-6 flex-wrap">
<div className="flex items-center gap-4 mb-6 flex-wrap relative z-10">
<div className="text-3xl md:text-4xl lg:text-5xl drop-shadow-[0_0_8px_rgba(255,255,255,0.2)] flex-shrink-0">
💰
{isEligibleForRewards ? (
<Sparkles className="w-10 h-10 md:w-12 md:h-12 text-yellow-400 animate-pulse" />
) : (
'💰'
)}
</div>
<div className="flex-1">
<h3 className="m-0 text-2xl md:text-3xl lg:text-4xl font-bold bg-gradient-to-r from-pink-400 via-purple-400 to-blue-400 bg-clip-text text-transparent break-words">
Collect Your Rewards
</h3>
<p className="text-sm md:text-base text-slate-400 mt-2 m-0">
{isEligibleForRewards
? "🎉 You're eligible! Withdraw your rewards anytime."
: 'Invite friends and earn rewards when they buy tokens'}
</p>
</div>
<h3 className="m-0 text-2xl md:text-3xl lg:text-4xl font-bold bg-gradient-to-r from-pink-400 via-purple-400 to-blue-400 bg-clip-text text-transparent break-words">
Collect your rewards
</h3>
</div>

{/* Content */}
<div className="flex flex-col lg:flex-row gap-8 lg:gap-12">
<div className="flex flex-col lg:flex-row gap-8 lg:gap-12 relative z-10">
{/* Description - Left Side */}
<div className="flex-1 space-y-4 text-sm text-muted-foreground">
<p>
Rewards accumulate as your direct invitees participate in token sales. You can withdraw once
{' '}
<span className="font-semibold text-white/80">
{MIN_INVITEES}
<div className="flex-1 space-y-4">
<div className="p-4 bg-gradient-to-r from-green-500/10 via-teal-500/10 to-blue-500/10 rounded-xl border border-white/10">
<p className="text-sm md:text-base text-white m-0 font-medium">
Once
{' '}
<span className="text-green-400 font-bold">
{MIN_INVITEES}
{' '}
direct invitees
</span>
{' '}
direct invitees
</span>
{' '}
have each
spent at least
<span className="font-semibold text-white/80">
{MIN_SPENT_AE}
have each spent at least
{' '}
AE
</span>
{' '}
(cumulative).
</p>
<p className="text-xs opacity-60">
<span className="text-green-400 font-bold">
{MIN_SPENT_AE}
{' '}
AE
</span>
{' '}
(cumulative) in token sales, you can withdraw accumulated rewards.
{' '}
Rewards build as your invitees participate.
</p>
</div>
<div className="space-y-2 text-sm text-slate-300">
<div className="flex items-start gap-2">
<TrendingUp className="w-4 h-4 text-green-400 mt-0.5 flex-shrink-0" />
<p className="m-0">Rewards reflect on-chain activity from your invitees</p>
</div>
<div className="flex items-start gap-2">
<span className="text-purple-400 mt-0.5">✓</span>
<p className="m-0">Withdraw after you meet the threshold and have a positive balance</p>
</div>
<div className="flex items-start gap-2">
<span className="text-blue-400 mt-0.5">✓</span>
<p className="m-0">Track per-invitee progress toward the spending minimum</p>
</div>
</div>
<p className="text-xs text-slate-400 opacity-80 m-0">
Note: eligibility and rewards depend on on-chain activity and are not guaranteed.
</p>
</div>

{/* Progress and Actions - Right Side */}
<div className="flex-1 flex flex-col gap-6">
{/* Progress Section */}
<div className="flex flex-col gap-3 p-4 bg-white/3 rounded-xl border border-white/5">
<div className="flex justify-between items-center font-semibold text-sm md:text-base flex-wrap gap-2">
<span>Progress to rewards</span>
<span className="text-teal-400 font-bold text-base md:text-lg text-shadow-[0_0_10px_rgba(78,205,196,0.5)] break-words">
<div className="flex flex-col gap-3 p-5 sm:p-6 bg-white/5 rounded-xl border border-white/10 relative overflow-hidden">
{isEligibleForRewards && (
<div className="absolute inset-0 bg-gradient-to-r from-green-500/20 via-teal-500/20 to-blue-500/20 animate-pulse" />
)}
<div className="flex justify-between items-center font-semibold text-sm md:text-base flex-wrap gap-2 relative z-10">
<span className="text-white">Progress to Rewards</span>
<span
className={`font-bold text-base md:text-lg break-words ${
thresholdReached
? 'text-green-400 text-shadow-[0_0_10px_rgba(34,197,94,0.5)]'
: 'text-teal-400 text-shadow-[0_0_10px_rgba(78,205,196,0.5)]'
}`}
>
{inviteesReachedCount}
/
{MIN_INVITEES}
{' '}
reached
</span>
</div>
<div className="w-full h-2 bg-white/10 rounded-md overflow-hidden relative before:content-[''] before:absolute before:inset-0 before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent before:animate-[shimmer_2s_infinite]">
<div className="w-full h-3 bg-white/10 rounded-full overflow-hidden relative before:content-[''] before:absolute before:inset-0 before:bg-gradient-to-r before:from-transparent before:via-white/10 before:to-transparent before:animate-[shimmer_2s_infinite]">
<div
className="h-full bg-gradient-to-r from-pink-500 to-purple-500 rounded-md transition-all duration-[800ms] ease-[cubic-bezier(0.4,0,0.2,1)] relative z-10"
className={`h-full rounded-full transition-all duration-[1000ms] ease-[cubic-bezier(0.4,0,0.2,1)] relative z-10 overflow-hidden ${
isEligibleForRewards
? 'bg-gradient-to-r from-green-500 via-teal-500 to-blue-500 shadow-lg shadow-green-500/50'
: 'bg-gradient-to-r from-pink-500 to-purple-500'
}`}
style={{ width: `${progressPercentage}%` }}
/>
>
{isEligibleForRewards && (
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-[shimmer_1s_infinite]" />
)}
</div>
</div>
<div className="text-center font-medium text-slate-400 text-xs md:text-sm p-1 rounded-lg bg-white/2 break-words">
{thresholdReached
? '🎉 Eligible to withdraw'
: `${Math.max(0, MIN_INVITEES - inviteesReachedCount)} more invitees need to reach ${MIN_SPENT_AE} AE`}
<div
className={`text-center font-medium text-xs md:text-sm p-2 rounded-lg break-words relative z-10 ${
thresholdReached
? 'bg-green-500/10 text-green-300 border border-white/10'
: 'bg-white/5 text-slate-400'
}`}
>
{thresholdReached ? (
<span className="flex items-center justify-center gap-2">
<Sparkles className="w-4 h-4 animate-pulse" />
Eligible to withdraw
</span>
) : (
`${Math.max(0, MIN_INVITEES - inviteesReachedCount)} more invitee${Math.max(0, MIN_INVITEES - inviteesReachedCount) === 1 ? '' : 's'} need to reach ${MIN_SPENT_AE} AE`
)}
</div>
{!thresholdReached && inviteesInProgressCount > 0 && (
<div className="text-center text-xs text-white/60">
Expand Down Expand Up @@ -372,15 +498,30 @@ const CollectRewardsCard = () => {

{/* Rewards Display */}
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2 text-center p-4 bg-white/3 rounded-xl border border-white/5">
<span className="text-xs md:text-sm text-slate-400 font-medium uppercase tracking-wider break-words">
<div
className={`flex flex-col gap-2 text-center p-5 sm:p-6 rounded-xl border relative overflow-hidden ${
isEligibleForRewards
? 'bg-gradient-to-br from-green-500/10 via-teal-500/10 to-blue-500/10 border-white/15'
: 'bg-white/5 border-white/10'
}`}
>
{isEligibleForRewards && (
<div className="absolute inset-0 bg-gradient-to-r from-green-500/10 via-teal-500/10 to-blue-500/10 animate-pulse" />
)}
<span className="text-xs md:text-sm text-slate-400 font-medium uppercase tracking-wider break-words relative z-10">
Available Rewards
</span>
<LivePriceFormatter
aePrice={Decimal.from(accumulatedRewardsAe.toString())}
watchPrice={false}
className="gap-2 items-center text-2xl md:text-3xl font-extrabold bg-gradient-to-r from-pink-400 via-purple-400 to-blue-400 bg-clip-text text-transparent text-shadow-[0_0_20px_rgba(255,107,107,0.3)] break-words"
/>
<div className="relative z-10">
<LivePriceFormatter
aePrice={Decimal.from(accumulatedRewardsAe.toString())}
watchPrice={false}
className={`gap-2 items-center text-3xl md:text-4xl lg:text-5xl font-extrabold break-words ${
isEligibleForRewards
? 'bg-gradient-to-r from-green-400 via-teal-400 to-blue-400 bg-clip-text text-transparent text-shadow-[0_0_20px_rgba(34,197,94,0.5)]'
: 'bg-gradient-to-r from-pink-400 via-purple-400 to-blue-400 bg-clip-text text-transparent text-shadow-[0_0_20px_rgba(255,107,107,0.3)]'
}`}
/>
</div>
</div>

{/* Error Message */}
Expand All @@ -396,9 +537,10 @@ const CollectRewardsCard = () => {
type="button"
onClick={onCollectReward}
disabled={collectingReward || !isEligibleForRewards}
className={`w-full p-4 md:p-5 lg:p-6 text-sm md:text-base font-bold uppercase tracking-wider break-words whitespace-normal min-h-12 rounded-xl transition-all duration-300 ${isEligibleForRewards
? 'bg-gradient-to-r from-pink-500 to-purple-500 text-white shadow-lg shadow-pink-500/30 hover:-translate-y-0.5 hover:shadow-xl hover:shadow-pink-500/40'
: 'opacity-50 cursor-not-allowed bg-gray-600 transform-none'
className={`w-full p-5 md:p-6 lg:p-7 text-sm md:text-base font-bold uppercase tracking-wider break-words whitespace-normal min-h-14 rounded-xl transition-all duration-300 relative overflow-hidden ${
isEligibleForRewards
? "bg-gradient-to-r from-green-500 via-teal-500 to-blue-500 text-white shadow-lg shadow-green-500/40 hover:-translate-y-1 hover:shadow-xl hover:shadow-green-500/50 before:content-[''] before:absolute before:top-0 before:-left-full before:w-full before:h-full before:bg-gradient-to-r before:from-transparent before:via-white/20 before:to-transparent before:transition-all before:duration-500 hover:before:left-full"
: 'opacity-50 cursor-not-allowed bg-gray-600 transform-none'
}`}
>
{collectButtonContent}
Expand Down
Loading
Loading