feat: integrate branding API and enhance referral sharing functionality

- Added branding API integration to fetch branding details.
- Updated share link functionality to include branding name in the share message.
This commit is contained in:
evansvl
2026-01-17 20:22:15 +03:00
parent 47714b0e8b
commit 9bb4809a3a
5 changed files with 3972 additions and 3878 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2,247 +2,341 @@ import { useState } from 'react'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { referralApi } from '../api/referral' import { referralApi } from '../api/referral'
import { brandingApi } from '../api/branding'
import { useCurrency } from '../hooks/useCurrency' import { useCurrency } from '../hooks/useCurrency'
const CopyIcon = () => ( const CopyIcon = () => (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> <svg
<path strokeLinecap="round" strokeLinejoin="round" d="M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184" /> className='w-4 h-4'
</svg> fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
strokeWidth={1.5}
>
<path
strokeLinecap='round'
strokeLinejoin='round'
d='M15.666 3.888A2.25 2.25 0 0013.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 01-.75.75H9a.75.75 0 01-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 01-2.25 2.25H6.75A2.25 2.25 0 014.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 011.927-.184'
/>
</svg>
) )
const CheckIcon = () => ( const CheckIcon = () => (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}> <svg
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" /> className='w-4 h-4'
</svg> fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
strokeWidth={2}
>
<path
strokeLinecap='round'
strokeLinejoin='round'
d='M4.5 12.75l6 6 9-13.5'
/>
</svg>
) )
const ShareIcon = () => ( const ShareIcon = () => (
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> <svg
<path strokeLinecap="round" strokeLinejoin="round" d="M7 8l5-5m0 0l5 5m-5-5v12" /> className='w-4 h-4'
<path strokeLinecap="round" strokeLinejoin="round" d="M4 15v3a2 2 0 002 2h12a2 2 0 002-2v-3" /> fill='none'
</svg> viewBox='0 0 24 24'
stroke='currentColor'
strokeWidth={1.5}
>
<path
strokeLinecap='round'
strokeLinejoin='round'
d='M7 8l5-5m0 0l5 5m-5-5v12'
/>
<path
strokeLinecap='round'
strokeLinejoin='round'
d='M4 15v3a2 2 0 002 2h12a2 2 0 002-2v-3'
/>
</svg>
) )
export default function Referral() { export default function Referral() {
const { t } = useTranslation() const { t } = useTranslation()
const { formatAmount, currencySymbol, formatPositive } = useCurrency() const { formatAmount, currencySymbol, formatPositive } = useCurrency()
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
const { data: info, isLoading } = useQuery({ const { data: info, isLoading } = useQuery({
queryKey: ['referral-info'], queryKey: ['referral-info'],
queryFn: referralApi.getReferralInfo, queryFn: referralApi.getReferralInfo,
}) })
const { data: terms } = useQuery({ const { data: terms } = useQuery({
queryKey: ['referral-terms'], queryKey: ['referral-terms'],
queryFn: referralApi.getReferralTerms, queryFn: referralApi.getReferralTerms,
}) })
const { data: referralList } = useQuery({ const { data: referralList } = useQuery({
queryKey: ['referral-list'], queryKey: ['referral-list'],
queryFn: () => referralApi.getReferralList({ per_page: 10 }), queryFn: () => referralApi.getReferralList({ per_page: 10 }),
}) })
const { data: earnings } = useQuery({ const { data: earnings } = useQuery({
queryKey: ['referral-earnings'], queryKey: ['referral-earnings'],
queryFn: () => referralApi.getReferralEarnings({ per_page: 10 }), queryFn: () => referralApi.getReferralEarnings({ per_page: 10 }),
}) })
const copyLink = () => { const { data: branding } = useQuery({
if (info?.referral_link) { queryKey: ['branding'],
navigator.clipboard.writeText(info.referral_link) queryFn: brandingApi.getBranding,
setCopied(true) staleTime: 60000,
setTimeout(() => setCopied(false), 2000) })
}
}
const shareLink = () => { const copyLink = () => {
if (!info?.referral_link) return if (info?.referral_link) {
const shareText = t('referral.shareMessage', { navigator.clipboard.writeText(info.referral_link)
percent: info?.commission_percent || 0, setCopied(true)
}) setTimeout(() => setCopied(false), 2000)
}
}
if (navigator.share) { const shareLink = () => {
navigator if (!info?.referral_link) return
.share({ const shareText = t('referral.shareMessage', {
title: t('referral.title'), percent: info?.commission_percent || 0,
text: shareText, botName: branding?.name || import.meta.env.VITE_APP_NAME || 'Cabinet',
url: info.referral_link, })
})
.catch(() => {
// ignore cancellation errors
})
return
}
const telegramUrl = `https://t.me/share/url?url=${encodeURIComponent( if (navigator.share) {
info.referral_link navigator
)}&text=${encodeURIComponent(shareText)}` .share({
window.open(telegramUrl, '_blank', 'noopener,noreferrer') title: t('referral.title'),
} text: shareText,
url: info.referral_link,
})
.catch(() => {
// ignore cancellation errors
})
return
}
if (isLoading) { const telegramUrl = `https://t.me/share/url?url=${encodeURIComponent(
return ( info.referral_link
<div className="flex items-center justify-center min-h-64"> )}&text=${encodeURIComponent(shareText)}`
<div className="w-10 h-10 border-2 border-accent-500 border-t-transparent rounded-full animate-spin" /> window.open(telegramUrl, '_blank', 'noopener,noreferrer')
</div> }
)
}
return ( if (isLoading) {
<div className="space-y-6"> return (
<h1 className="text-2xl sm:text-3xl font-bold text-dark-50">{t('referral.title')}</h1> <div className='flex items-center justify-center min-h-64'>
<div className='w-10 h-10 border-2 border-accent-500 border-t-transparent rounded-full animate-spin' />
</div>
)
}
{/* Stats Cards */} return (
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4"> <div className='space-y-6'>
<div className="card"> <h1 className='text-2xl sm:text-3xl font-bold text-dark-50'>
<div className="text-sm text-dark-400">{t('referral.stats.totalReferrals')}</div> {t('referral.title')}
<div className="stat-value mt-1">{info?.total_referrals || 0}</div> </h1>
<div className="text-sm text-dark-500 mt-1">
{info?.active_referrals || 0} {t('referral.stats.activeReferrals').toLowerCase()}
</div>
</div>
<div className="card">
<div className="text-sm text-dark-400">{t('referral.stats.totalEarnings')}</div>
<div className="stat-value text-success-400 mt-1">
{formatPositive(info?.total_earnings_rubles || 0)}
</div>
</div>
<div className="card col-span-2 sm:col-span-1">
<div className="text-sm text-dark-400">{t('referral.stats.commissionRate')}</div>
<div className="stat-value text-accent-400 mt-1">
{info?.commission_percent || 0}%
</div>
</div>
</div>
{/* Referral Link */} {/* Stats Cards */}
<div className="card"> <div className='grid grid-cols-2 sm:grid-cols-3 gap-4'>
<h2 className="text-lg font-semibold text-dark-100 mb-4">{t('referral.yourLink')}</h2> <div className='card'>
<div className="flex flex-col gap-2 sm:flex-row"> <div className='text-sm text-dark-400'>
<input {t('referral.stats.totalReferrals')}
type="text" </div>
readOnly <div className='stat-value mt-1'>{info?.total_referrals || 0}</div>
value={info?.referral_link || ''} <div className='text-sm text-dark-500 mt-1'>
className="input flex-1" {info?.active_referrals || 0}{' '}
/> {t('referral.stats.activeReferrals').toLowerCase()}
<div className="flex gap-2"> </div>
<button </div>
onClick={copyLink} <div className='card'>
disabled={!info?.referral_link} <div className='text-sm text-dark-400'>
className={`btn-primary px-5 ${ {t('referral.stats.totalEarnings')}
copied ? 'bg-success-500 hover:bg-success-500' : '' </div>
} ${!info?.referral_link ? 'opacity-50 cursor-not-allowed' : ''}`} <div className='stat-value text-success-400 mt-1'>
> {formatPositive(info?.total_earnings_rubles || 0)}
{copied ? <CheckIcon /> : <CopyIcon />} </div>
<span className="ml-2">{copied ? t('referral.copied') : t('referral.copyLink')}</span> </div>
</button> <div className='card col-span-2 sm:col-span-1'>
<button <div className='text-sm text-dark-400'>
onClick={shareLink} {t('referral.stats.commissionRate')}
disabled={!info?.referral_link} </div>
className={`btn-secondary px-5 flex items-center ${ <div className='stat-value text-accent-400 mt-1'>
!info?.referral_link ? 'opacity-50 cursor-not-allowed' : '' {info?.commission_percent || 0}%
}`} </div>
> </div>
<ShareIcon /> </div>
<span className="ml-2">{t('referral.shareButton')}</span>
</button>
</div>
</div>
<p className="mt-3 text-sm text-dark-500">
{t('referral.shareHint', { percent: info?.commission_percent || 0 })}
</p>
</div>
{/* Program Terms */} {/* Referral Link */}
{terms && ( <div className='card'>
<div className="card"> <h2 className='text-lg font-semibold text-dark-100 mb-4'>
<h2 className="text-lg font-semibold text-dark-100 mb-4">{t('referral.terms.title')}</h2> {t('referral.yourLink')}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> </h2>
<div className="p-3 rounded-xl bg-dark-800/30"> <div className='flex flex-col gap-2 sm:flex-row'>
<div className="text-sm text-dark-500">{t('referral.terms.commission')}</div> <input
<div className="text-lg font-semibold text-dark-100 mt-1">{terms.commission_percent}%</div> type='text'
</div> readOnly
<div className="p-3 rounded-xl bg-dark-800/30"> value={info?.referral_link || ''}
<div className="text-sm text-dark-500">{t('referral.terms.minTopup')}</div> className='input flex-1'
<div className="text-lg font-semibold text-dark-100 mt-1">{formatAmount(terms.minimum_topup_rubles)} {currencySymbol}</div> />
</div> <div className='flex gap-2'>
<div className="p-3 rounded-xl bg-dark-800/30"> <button
<div className="text-sm text-dark-500">{t('referral.terms.newUserBonus')}</div> onClick={copyLink}
<div className="text-lg font-semibold text-success-400 mt-1">{formatPositive(terms.first_topup_bonus_rubles)}</div> disabled={!info?.referral_link}
</div> className={`btn-primary px-5 ${
<div className="p-3 rounded-xl bg-dark-800/30"> copied ? 'bg-success-500 hover:bg-success-500' : ''
<div className="text-sm text-dark-500">{t('referral.terms.inviterBonus')}</div> } ${!info?.referral_link ? 'opacity-50 cursor-not-allowed' : ''}`}
<div className="text-lg font-semibold text-success-400 mt-1">{formatPositive(terms.inviter_bonus_rubles)}</div> >
</div> {copied ? <CheckIcon /> : <CopyIcon />}
</div> <span className='ml-2'>
</div> {copied ? t('referral.copied') : t('referral.copyLink')}
)} </span>
</button>
<button
onClick={shareLink}
disabled={!info?.referral_link}
className={`btn-secondary px-5 flex items-center ${
!info?.referral_link ? 'opacity-50 cursor-not-allowed' : ''
}`}
>
<ShareIcon />
<span className='ml-2'>{t('referral.shareButton')}</span>
</button>
</div>
</div>
<p className='mt-3 text-sm text-dark-500'>
{t('referral.shareHint', { percent: info?.commission_percent || 0 })}
</p>
</div>
{/* Referrals List */} {/* Program Terms */}
<div className="card"> {terms && (
<h2 className="text-lg font-semibold text-dark-100 mb-4">{t('referral.yourReferrals')}</h2> <div className='card'>
{referralList?.items && referralList.items.length > 0 ? ( <h2 className='text-lg font-semibold text-dark-100 mb-4'>
<div className="space-y-3"> {t('referral.terms.title')}
{referralList.items.map((ref) => ( </h2>
<div <div className='grid grid-cols-2 md:grid-cols-4 gap-4'>
key={ref.id} <div className='p-3 rounded-xl bg-dark-800/30'>
className="flex items-center justify-between p-3 rounded-xl bg-dark-800/30 border border-dark-700/30" <div className='text-sm text-dark-500'>
> {t('referral.terms.commission')}
<div> </div>
<div className="text-dark-100 font-medium"> <div className='text-lg font-semibold text-dark-100 mt-1'>
{ref.first_name || ref.username || `User #${ref.id}`} {terms.commission_percent}%
</div> </div>
<div className="text-xs text-dark-500 mt-0.5"> </div>
{new Date(ref.created_at).toLocaleDateString()} <div className='p-3 rounded-xl bg-dark-800/30'>
</div> <div className='text-sm text-dark-500'>
</div> {t('referral.terms.minTopup')}
{ref.has_paid ? ( </div>
<span className="badge-success">{t('referral.status.paid')}</span> <div className='text-lg font-semibold text-dark-100 mt-1'>
) : ( {formatAmount(terms.minimum_topup_rubles)} {currencySymbol}
<span className="badge-neutral">{t('referral.status.pending')}</span> </div>
)} </div>
</div> <div className='p-3 rounded-xl bg-dark-800/30'>
))} <div className='text-sm text-dark-500'>
</div> {t('referral.terms.newUserBonus')}
) : ( </div>
<div className="text-center py-12"> <div className='text-lg font-semibold text-success-400 mt-1'>
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-dark-800 flex items-center justify-center"> {formatPositive(terms.first_topup_bonus_rubles)}
<svg className="w-8 h-8 text-dark-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> </div>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" /> </div>
</svg> <div className='p-3 rounded-xl bg-dark-800/30'>
</div> <div className='text-sm text-dark-500'>
<div className="text-dark-400">{t('referral.noReferrals')}</div> {t('referral.terms.inviterBonus')}
</div> </div>
)} <div className='text-lg font-semibold text-success-400 mt-1'>
</div> {formatPositive(terms.inviter_bonus_rubles)}
</div>
</div>
</div>
</div>
)}
{/* Earnings History */} {/* Referrals List */}
{earnings?.items && earnings.items.length > 0 && ( <div className='card'>
<div className="card"> <h2 className='text-lg font-semibold text-dark-100 mb-4'>
<h2 className="text-lg font-semibold text-dark-100 mb-4">{t('referral.earningsHistory')}</h2> {t('referral.yourReferrals')}
<div className="space-y-3"> </h2>
{earnings.items.map((earning) => ( {referralList?.items && referralList.items.length > 0 ? (
<div <div className='space-y-3'>
key={earning.id} {referralList.items.map(ref => (
className="flex items-center justify-between p-3 rounded-xl bg-dark-800/30 border border-dark-700/30" <div
> key={ref.id}
<div> className='flex items-center justify-between p-3 rounded-xl bg-dark-800/30 border border-dark-700/30'
<div className="text-dark-100"> >
{earning.referral_first_name || earning.referral_username || 'Referral'} <div>
</div> <div className='text-dark-100 font-medium'>
<div className="text-xs text-dark-500 mt-0.5"> {ref.first_name || ref.username || `User #${ref.id}`}
{t(`referral.reasons.${earning.reason}`, earning.reason)} {new Date(earning.created_at).toLocaleDateString()} </div>
</div> <div className='text-xs text-dark-500 mt-0.5'>
</div> {new Date(ref.created_at).toLocaleDateString()}
<div className="text-success-400 font-semibold"> </div>
{formatPositive(earning.amount_rubles)} </div>
</div> {ref.has_paid ? (
</div> <span className='badge-success'>
))} {t('referral.status.paid')}
</div> </span>
</div> ) : (
)} <span className='badge-neutral'>
</div> {t('referral.status.pending')}
) </span>
)}
</div>
))}
</div>
) : (
<div className='text-center py-12'>
<div className='w-16 h-16 mx-auto mb-4 rounded-2xl bg-dark-800 flex items-center justify-center'>
<svg
className='w-8 h-8 text-dark-500'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
strokeWidth={1.5}
>
<path
strokeLinecap='round'
strokeLinejoin='round'
d='M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z'
/>
</svg>
</div>
<div className='text-dark-400'>{t('referral.noReferrals')}</div>
</div>
)}
</div>
{/* Earnings History */}
{earnings?.items && earnings.items.length > 0 && (
<div className='card'>
<h2 className='text-lg font-semibold text-dark-100 mb-4'>
{t('referral.earningsHistory')}
</h2>
<div className='space-y-3'>
{earnings.items.map(earning => (
<div
key={earning.id}
className='flex items-center justify-between p-3 rounded-xl bg-dark-800/30 border border-dark-700/30'
>
<div>
<div className='text-dark-100'>
{earning.referral_first_name ||
earning.referral_username ||
'Referral'}
</div>
<div className='text-xs text-dark-500 mt-0.5'>
{t(`referral.reasons.${earning.reason}`, earning.reason)} {' '}
{new Date(earning.created_at).toLocaleDateString()}
</div>
</div>
<div className='text-success-400 font-semibold'>
{formatPositive(earning.amount_rubles)}
</div>
</div>
))}
</div>
</div>
)}
</div>
)
} }