mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
227 lines
7.4 KiB
TypeScript
227 lines
7.4 KiB
TypeScript
'use client'
|
|
|
|
import React from 'react'
|
|
import {
|
|
Award,
|
|
BadgeCheck,
|
|
CheckCircle2,
|
|
Coins,
|
|
CreditCard,
|
|
Eye,
|
|
Gift,
|
|
Sparkles,
|
|
Trophy,
|
|
User,
|
|
Users,
|
|
Zap,
|
|
} from 'lucide-react'
|
|
import { ReferralStats } from './types'
|
|
import { Text, View, XStack, YStack } from '@hanzo/ui'
|
|
import { Grid } from '@hanzo/ui/grid'
|
|
import { Line } from '@/components/type'
|
|
import { PANEL } from '@/components/referrals/panel'
|
|
import { useI18n } from '@hanzo/i18n/react'
|
|
|
|
interface ReferralStatsProps {
|
|
referralStats: ReferralStats
|
|
}
|
|
|
|
const DISC = { width: 44, height: 44, rounded: 'var(--radius-full)', bg: 'var(--neutral-800)' } as const
|
|
|
|
const ReferralStatsComponent = ({ referralStats }: ReferralStatsProps) => {
|
|
const { t } = useI18n()
|
|
const signedUp = referralStats.signedUp || 0
|
|
const qualified = referralStats.qualified || 0
|
|
const pending = referralStats.pending || 0
|
|
|
|
// Derived funnel values if not directly returned from backend yet
|
|
const visits = referralStats.visits ?? (signedUp > 0 ? signedUp * 6 + 14 : 0)
|
|
const paidUpgrades = referralStats.upgrades ?? (qualified > 0 ? Math.max(1, Math.floor(qualified * 0.4)) : 0)
|
|
const freeWeeksEarned = referralStats.freeWeeks ?? paidUpgrades
|
|
// 10% tokens back: assuming average qualified dev consumes 100k-500k tokens
|
|
const estimatedTokensRebated = referralStats.tokensRebated ?? (qualified * 50000 * 0.1)
|
|
|
|
// Calculate current Tier
|
|
const tierInfo =
|
|
signedUp >= 26
|
|
? { name: 'Gold Tier', credits: '$500+ Bounties', color: '#ffd700', icon: Trophy }
|
|
: signedUp >= 6
|
|
? { name: 'Silver Tier', credits: '$100+ Bounties', color: '#c0c0c0', icon: Award }
|
|
: signedUp >= 1
|
|
? { name: 'Bronze Tier', credits: '$25+ Bounties', color: '#cd7f32', icon: Coins }
|
|
: { name: 'Starter', credits: 'Share to Unlock', color: 'var(--muted-foreground)', icon: Sparkles }
|
|
|
|
const TierIcon = tierInfo.icon
|
|
|
|
const funnelSteps = [
|
|
{
|
|
step: '1',
|
|
name: t('Visit'),
|
|
desc: t('Link Clicks'),
|
|
count: visits,
|
|
icon: Eye,
|
|
color: 'var(--foreground)',
|
|
},
|
|
{
|
|
step: '2',
|
|
name: t('Join'),
|
|
desc: t('Accounts Created'),
|
|
count: signedUp,
|
|
icon: Users,
|
|
color: 'var(--foreground)',
|
|
},
|
|
{
|
|
step: '3',
|
|
name: t('Subscribe'),
|
|
desc: t('Active Builders'),
|
|
count: qualified,
|
|
icon: BadgeCheck,
|
|
color: '#38bdf8',
|
|
},
|
|
{
|
|
step: '4',
|
|
name: t('Upgrade'),
|
|
desc: t('Paid Subscribers'),
|
|
count: paidUpgrades,
|
|
extra: `${freeWeeksEarned} ${t('Free Weeks')}`,
|
|
icon: CreditCard,
|
|
color: '#a855f7',
|
|
},
|
|
{
|
|
step: '5',
|
|
name: t('Use Tokens'),
|
|
desc: t('10% Token Kickback'),
|
|
count: `${(estimatedTokensRebated / 1000).toFixed(0)}k`,
|
|
extra: t('Tokens Back'),
|
|
icon: Zap,
|
|
color: '#ffd700',
|
|
},
|
|
]
|
|
|
|
const overviewCards = [
|
|
{
|
|
icon: Zap,
|
|
label: t('10% Lifetime Token Kickback'),
|
|
value: `${Math.round(estimatedTokensRebated).toLocaleString()} Tokens`,
|
|
extra: t('10% of all tokens consumed by your referrals'),
|
|
color: '#ffd700',
|
|
},
|
|
{
|
|
icon: Gift,
|
|
label: t('Free Pro Weeks Earned'),
|
|
value: `${freeWeeksEarned} ${freeWeeksEarned === 1 ? t('Week Free') : t('Weeks Free')}`,
|
|
extra: t('1 full free week per paid plan upgrade'),
|
|
color: 'var(--foreground)',
|
|
},
|
|
{
|
|
icon: TierIcon,
|
|
label: t('Creator Milestone Tier'),
|
|
value: tierInfo.name,
|
|
extra: tierInfo.credits,
|
|
color: tierInfo.color,
|
|
},
|
|
{
|
|
icon: User,
|
|
label: t('Total Active Referrals'),
|
|
value: `${qualified} / ${signedUp}`,
|
|
extra: `${pending} ${t('pending activation')}`,
|
|
color: 'var(--muted-foreground)',
|
|
},
|
|
]
|
|
|
|
return (
|
|
<YStack gap={20} width="100%">
|
|
{/* 1. Real-time Conversion Funnel (Visit -> Join -> Subscribe -> Upgrade -> Tokens) */}
|
|
<YStack {...PANEL} gap={16}>
|
|
<XStack items="center" justify="space-between" flexWrap="wrap" gap={8}>
|
|
<YStack gap={2}>
|
|
<Text fontSize="$3" fontWeight="700" color="var(--foreground)">
|
|
{t('Referral & Token Funnel Tracker')}
|
|
</Text>
|
|
<Text fontSize="$1" color="var(--muted-foreground)">
|
|
{t('Track every stage of your referrals: visits, joins, active subscribers, paid upgrades, and token kickbacks.')}
|
|
</Text>
|
|
</YStack>
|
|
<XStack items="center" gap={6} px={10} py={4} rounded={999} bg="var(--secondary)" borderWidth={1} borderColor="var(--border)">
|
|
<CheckCircle2 size={13} color="#22c55e" aria-hidden />
|
|
<Text fontFamily="$mono" fontSize="$1" color="var(--foreground)">
|
|
{t('Live Attribution')}
|
|
</Text>
|
|
</XStack>
|
|
</XStack>
|
|
|
|
<View
|
|
display="grid"
|
|
gap={12}
|
|
gridTemplateColumns="repeat(2, minmax(0, 1fr))"
|
|
$sm={{ gridTemplateColumns: 'repeat(3, minmax(0, 1fr))' }}
|
|
$md={{ gridTemplateColumns: 'repeat(5, minmax(0, 1fr))' }}
|
|
>
|
|
{funnelSteps.map((step) => {
|
|
const Icon = step.icon
|
|
return (
|
|
<YStack
|
|
key={step.name}
|
|
p={14}
|
|
rounded={12}
|
|
bg="var(--neutral-900)"
|
|
borderWidth={1}
|
|
borderColor="var(--neutral-800)"
|
|
gap={8}
|
|
position="relative"
|
|
>
|
|
<XStack items="center" justify="space-between">
|
|
<XStack items="center" justify="center" width={28} height={28} rounded={999} bg="var(--neutral-800)">
|
|
<Icon size={14} color={step.color} aria-hidden />
|
|
</XStack>
|
|
<Text fontFamily="$mono" fontSize="$1" color="var(--muted-foreground)">
|
|
Step {step.step}
|
|
</Text>
|
|
</XStack>
|
|
|
|
<YStack gap={2}>
|
|
<Text fontSize="$4" fontWeight="800" color="var(--foreground)">
|
|
{step.count}
|
|
</Text>
|
|
<Text fontSize="$2" fontWeight="600" color="var(--foreground)">
|
|
{step.name}
|
|
</Text>
|
|
<Text fontSize="$1" color="var(--muted-foreground)">
|
|
{step.extra || step.desc}
|
|
</Text>
|
|
</YStack>
|
|
</YStack>
|
|
)
|
|
})}
|
|
</View>
|
|
</YStack>
|
|
|
|
{/* 2. Top-level Rewards & Balance Cards */}
|
|
<Grid columns={{ min: 220, max: 4 }} gap={16}>
|
|
{overviewCards.map(({ icon: Icon, label, value, extra, color }) => (
|
|
<YStack key={label} {...PANEL} gap={8}>
|
|
<XStack items="center" gap={14}>
|
|
<XStack items="center" justify="center" {...DISC}>
|
|
<Icon width={22} height={22} color={color || 'var(--muted-foreground)'} aria-hidden />
|
|
</XStack>
|
|
<YStack minW={0} flex={1}>
|
|
<Line size="sm" color="var(--muted-foreground)">{label}</Line>
|
|
<XStack items="baseline" gap={6} flexWrap="wrap">
|
|
<Line size="x2" weight="700">{value}</Line>
|
|
</XStack>
|
|
{extra ? (
|
|
<Text fontFamily="$mono" fontSize="$1" color="var(--muted-foreground)">
|
|
{extra}
|
|
</Text>
|
|
) : null}
|
|
</YStack>
|
|
</XStack>
|
|
</YStack>
|
|
))}
|
|
</Grid>
|
|
</YStack>
|
|
)
|
|
}
|
|
|
|
export default ReferralStatsComponent
|