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

View File

@@ -319,7 +319,7 @@
"copyLink": "Copy Link", "copyLink": "Copy Link",
"copied": "Copied!", "copied": "Copied!",
"shareButton": "Share", "shareButton": "Share",
"shareMessage": "Join me on and earn up to {{percent}}% cashback!", "shareMessage": "Join me on {{botName}} and earn up to {{percent}}% cashback!",
"shareHint": "Share this link with friends. When they sign up and make a purchase, you'll earn {{percent}}% commission!", "shareHint": "Share this link with friends. When they sign up and make a purchase, you'll earn {{percent}}% commission!",
"stats": { "stats": {
"totalReferrals": "Total Referrals", "totalReferrals": "Total Referrals",

View File

@@ -220,7 +220,7 @@
"copyLink": "کپی لینک", "copyLink": "کپی لینک",
"copied": "کپی شد!", "copied": "کپی شد!",
"shareButton": "اشتراک‌گذاری", "shareButton": "اشتراک‌گذاری",
"shareMessage": "از طریق لینک من به Cabinet بپیوند و تا {{percent}}٪ پاداش بگیر!", "shareMessage": "از طریق لینک من به {{botName}} بپیوند و تا {{percent}}٪ پاداش بگیر!",
"shareHint": "این لینک را با دوستان به اشتراک بگذارید. وقتی ثبت نام کنند و خرید کنند، {{percent}}% کمیسیون دریافت می‌کنید!", "shareHint": "این لینک را با دوستان به اشتراک بگذارید. وقتی ثبت نام کنند و خرید کنند، {{percent}}% کمیسیون دریافت می‌کنید!",
"stats": { "stats": {
"totalReferrals": "کل معرفی‌ها", "totalReferrals": "کل معرفی‌ها",

View File

@@ -319,7 +319,7 @@
"copyLink": "Копировать ссылку", "copyLink": "Копировать ссылку",
"copied": "Скопировано!", "copied": "Скопировано!",
"shareButton": "Поделиться", "shareButton": "Поделиться",
"shareMessage": "Заходи в Cabinet по моей ссылке и получай до {{percent}}% кешбэка!", "shareMessage": "Заходи в {{botName}} по моей ссылке и получай до {{percent}}% кешбэка!",
"shareHint": "Поделитесь ссылкой с друзьями. Когда они зарегистрируются и оплатят, вы получите {{percent}}% комиссии!", "shareHint": "Поделитесь ссылкой с друзьями. Когда они зарегистрируются и оплатят, вы получите {{percent}}% комиссии!",
"stats": { "stats": {
"totalReferrals": "Всего рефералов", "totalReferrals": "Всего рефералов",

View File

@@ -220,7 +220,7 @@
"copyLink": "复制链接", "copyLink": "复制链接",
"copied": "已复制!", "copied": "已复制!",
"shareButton": "分享", "shareButton": "分享",
"shareMessage": "通过我的链接加入 Cabinet,获取高达 {{percent}}% 返现!", "shareMessage": "通过我的链接加入 {{botName}},获取高达 {{percent}}% 返现!",
"shareHint": "分享链接给朋友。当他们注册并付款后,您将获得 {{percent}}% 佣金!", "shareHint": "分享链接给朋友。当他们注册并付款后,您将获得 {{percent}}% 佣金!",
"stats": { "stats": {
"totalReferrals": "总推荐数", "totalReferrals": "总推荐数",

View File

@@ -2,24 +2,59 @@ 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'
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> </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'
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> </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'
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> </svg>
) )
@@ -48,6 +83,12 @@ export default function Referral() {
queryFn: () => referralApi.getReferralEarnings({ per_page: 10 }), queryFn: () => referralApi.getReferralEarnings({ per_page: 10 }),
}) })
const { data: branding } = useQuery({
queryKey: ['branding'],
queryFn: brandingApi.getBranding,
staleTime: 60000,
})
const copyLink = () => { const copyLink = () => {
if (info?.referral_link) { if (info?.referral_link) {
navigator.clipboard.writeText(info.referral_link) navigator.clipboard.writeText(info.referral_link)
@@ -60,6 +101,7 @@ export default function Referral() {
if (!info?.referral_link) return if (!info?.referral_link) return
const shareText = t('referral.shareMessage', { const shareText = t('referral.shareMessage', {
percent: info?.commission_percent || 0, percent: info?.commission_percent || 0,
botName: branding?.name || import.meta.env.VITE_APP_NAME || 'Cabinet',
}) })
if (navigator.share) { if (navigator.share) {
@@ -83,50 +125,61 @@ export default function Referral() {
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex items-center justify-center min-h-64"> <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 className='w-10 h-10 border-2 border-accent-500 border-t-transparent rounded-full animate-spin' />
</div> </div>
) )
} }
return ( return (
<div className="space-y-6"> <div className='space-y-6'>
<h1 className="text-2xl sm:text-3xl font-bold text-dark-50">{t('referral.title')}</h1> <h1 className='text-2xl sm:text-3xl font-bold text-dark-50'>
{t('referral.title')}
</h1>
{/* Stats Cards */} {/* Stats Cards */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4"> <div className='grid grid-cols-2 sm:grid-cols-3 gap-4'>
<div className="card"> <div className='card'>
<div className="text-sm text-dark-400">{t('referral.stats.totalReferrals')}</div> <div className='text-sm text-dark-400'>
<div className="stat-value mt-1">{info?.total_referrals || 0}</div> {t('referral.stats.totalReferrals')}
<div className="text-sm text-dark-500 mt-1"> </div>
{info?.active_referrals || 0} {t('referral.stats.activeReferrals').toLowerCase()} <div className='stat-value mt-1'>{info?.total_referrals || 0}</div>
<div className='text-sm text-dark-500 mt-1'>
{info?.active_referrals || 0}{' '}
{t('referral.stats.activeReferrals').toLowerCase()}
</div> </div>
</div> </div>
<div className="card"> <div className='card'>
<div className="text-sm text-dark-400">{t('referral.stats.totalEarnings')}</div> <div className='text-sm text-dark-400'>
<div className="stat-value text-success-400 mt-1"> {t('referral.stats.totalEarnings')}
</div>
<div className='stat-value text-success-400 mt-1'>
{formatPositive(info?.total_earnings_rubles || 0)} {formatPositive(info?.total_earnings_rubles || 0)}
</div> </div>
</div> </div>
<div className="card col-span-2 sm:col-span-1"> <div className='card col-span-2 sm:col-span-1'>
<div className="text-sm text-dark-400">{t('referral.stats.commissionRate')}</div> <div className='text-sm text-dark-400'>
<div className="stat-value text-accent-400 mt-1"> {t('referral.stats.commissionRate')}
</div>
<div className='stat-value text-accent-400 mt-1'>
{info?.commission_percent || 0}% {info?.commission_percent || 0}%
</div> </div>
</div> </div>
</div> </div>
{/* Referral Link */} {/* Referral Link */}
<div className="card"> <div className='card'>
<h2 className="text-lg font-semibold text-dark-100 mb-4">{t('referral.yourLink')}</h2> <h2 className='text-lg font-semibold text-dark-100 mb-4'>
<div className="flex flex-col gap-2 sm:flex-row"> {t('referral.yourLink')}
</h2>
<div className='flex flex-col gap-2 sm:flex-row'>
<input <input
type="text" type='text'
readOnly readOnly
value={info?.referral_link || ''} value={info?.referral_link || ''}
className="input flex-1" className='input flex-1'
/> />
<div className="flex gap-2"> <div className='flex gap-2'>
<button <button
onClick={copyLink} onClick={copyLink}
disabled={!info?.referral_link} disabled={!info?.referral_link}
@@ -135,7 +188,9 @@ export default function Referral() {
} ${!info?.referral_link ? 'opacity-50 cursor-not-allowed' : ''}`} } ${!info?.referral_link ? 'opacity-50 cursor-not-allowed' : ''}`}
> >
{copied ? <CheckIcon /> : <CopyIcon />} {copied ? <CheckIcon /> : <CopyIcon />}
<span className="ml-2">{copied ? t('referral.copied') : t('referral.copyLink')}</span> <span className='ml-2'>
{copied ? t('referral.copied') : t('referral.copyLink')}
</span>
</button> </button>
<button <button
onClick={shareLink} onClick={shareLink}
@@ -145,97 +200,136 @@ export default function Referral() {
}`} }`}
> >
<ShareIcon /> <ShareIcon />
<span className="ml-2">{t('referral.shareButton')}</span> <span className='ml-2'>{t('referral.shareButton')}</span>
</button> </button>
</div> </div>
</div> </div>
<p className="mt-3 text-sm text-dark-500"> <p className='mt-3 text-sm text-dark-500'>
{t('referral.shareHint', { percent: info?.commission_percent || 0 })} {t('referral.shareHint', { percent: info?.commission_percent || 0 })}
</p> </p>
</div> </div>
{/* Program Terms */} {/* Program Terms */}
{terms && ( {terms && (
<div className="card"> <div className='card'>
<h2 className="text-lg font-semibold text-dark-100 mb-4">{t('referral.terms.title')}</h2> <h2 className='text-lg font-semibold text-dark-100 mb-4'>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> {t('referral.terms.title')}
<div className="p-3 rounded-xl bg-dark-800/30"> </h2>
<div className="text-sm text-dark-500">{t('referral.terms.commission')}</div> <div className='grid grid-cols-2 md:grid-cols-4 gap-4'>
<div className="text-lg font-semibold text-dark-100 mt-1">{terms.commission_percent}%</div> <div className='p-3 rounded-xl bg-dark-800/30'>
<div className='text-sm text-dark-500'>
{t('referral.terms.commission')}
</div> </div>
<div className="p-3 rounded-xl bg-dark-800/30"> <div className='text-lg font-semibold text-dark-100 mt-1'>
<div className="text-sm text-dark-500">{t('referral.terms.minTopup')}</div> {terms.commission_percent}%
<div className="text-lg font-semibold text-dark-100 mt-1">{formatAmount(terms.minimum_topup_rubles)} {currencySymbol}</div>
</div> </div>
<div className="p-3 rounded-xl bg-dark-800/30">
<div className="text-sm text-dark-500">{t('referral.terms.newUserBonus')}</div>
<div className="text-lg font-semibold text-success-400 mt-1">{formatPositive(terms.first_topup_bonus_rubles)}</div>
</div> </div>
<div className="p-3 rounded-xl bg-dark-800/30"> <div className='p-3 rounded-xl bg-dark-800/30'>
<div className="text-sm text-dark-500">{t('referral.terms.inviterBonus')}</div> <div className='text-sm text-dark-500'>
<div className="text-lg font-semibold text-success-400 mt-1">{formatPositive(terms.inviter_bonus_rubles)}</div> {t('referral.terms.minTopup')}
</div>
<div className='text-lg font-semibold text-dark-100 mt-1'>
{formatAmount(terms.minimum_topup_rubles)} {currencySymbol}
</div>
</div>
<div className='p-3 rounded-xl bg-dark-800/30'>
<div className='text-sm text-dark-500'>
{t('referral.terms.newUserBonus')}
</div>
<div className='text-lg font-semibold text-success-400 mt-1'>
{formatPositive(terms.first_topup_bonus_rubles)}
</div>
</div>
<div className='p-3 rounded-xl bg-dark-800/30'>
<div className='text-sm text-dark-500'>
{t('referral.terms.inviterBonus')}
</div>
<div className='text-lg font-semibold text-success-400 mt-1'>
{formatPositive(terms.inviter_bonus_rubles)}
</div>
</div> </div>
</div> </div>
</div> </div>
)} )}
{/* Referrals List */} {/* Referrals List */}
<div className="card"> <div className='card'>
<h2 className="text-lg font-semibold text-dark-100 mb-4">{t('referral.yourReferrals')}</h2> <h2 className='text-lg font-semibold text-dark-100 mb-4'>
{t('referral.yourReferrals')}
</h2>
{referralList?.items && referralList.items.length > 0 ? ( {referralList?.items && referralList.items.length > 0 ? (
<div className="space-y-3"> <div className='space-y-3'>
{referralList.items.map((ref) => ( {referralList.items.map(ref => (
<div <div
key={ref.id} key={ref.id}
className="flex items-center justify-between p-3 rounded-xl bg-dark-800/30 border border-dark-700/30" className='flex items-center justify-between p-3 rounded-xl bg-dark-800/30 border border-dark-700/30'
> >
<div> <div>
<div className="text-dark-100 font-medium"> <div className='text-dark-100 font-medium'>
{ref.first_name || ref.username || `User #${ref.id}`} {ref.first_name || ref.username || `User #${ref.id}`}
</div> </div>
<div className="text-xs text-dark-500 mt-0.5"> <div className='text-xs text-dark-500 mt-0.5'>
{new Date(ref.created_at).toLocaleDateString()} {new Date(ref.created_at).toLocaleDateString()}
</div> </div>
</div> </div>
{ref.has_paid ? ( {ref.has_paid ? (
<span className="badge-success">{t('referral.status.paid')}</span> <span className='badge-success'>
{t('referral.status.paid')}
</span>
) : ( ) : (
<span className="badge-neutral">{t('referral.status.pending')}</span> <span className='badge-neutral'>
{t('referral.status.pending')}
</span>
)} )}
</div> </div>
))} ))}
</div> </div>
) : ( ) : (
<div className="text-center py-12"> <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"> <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}> <svg
<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" /> 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> </svg>
</div> </div>
<div className="text-dark-400">{t('referral.noReferrals')}</div> <div className='text-dark-400'>{t('referral.noReferrals')}</div>
</div> </div>
)} )}
</div> </div>
{/* Earnings History */} {/* Earnings History */}
{earnings?.items && earnings.items.length > 0 && ( {earnings?.items && earnings.items.length > 0 && (
<div className="card"> <div className='card'>
<h2 className="text-lg font-semibold text-dark-100 mb-4">{t('referral.earningsHistory')}</h2> <h2 className='text-lg font-semibold text-dark-100 mb-4'>
<div className="space-y-3"> {t('referral.earningsHistory')}
{earnings.items.map((earning) => ( </h2>
<div className='space-y-3'>
{earnings.items.map(earning => (
<div <div
key={earning.id} key={earning.id}
className="flex items-center justify-between p-3 rounded-xl bg-dark-800/30 border border-dark-700/30" className='flex items-center justify-between p-3 rounded-xl bg-dark-800/30 border border-dark-700/30'
> >
<div> <div>
<div className="text-dark-100"> <div className='text-dark-100'>
{earning.referral_first_name || earning.referral_username || 'Referral'} {earning.referral_first_name ||
earning.referral_username ||
'Referral'}
</div> </div>
<div className="text-xs text-dark-500 mt-0.5"> <div className='text-xs text-dark-500 mt-0.5'>
{t(`referral.reasons.${earning.reason}`, earning.reason)} {new Date(earning.created_at).toLocaleDateString()} {t(`referral.reasons.${earning.reason}`, earning.reason)} {' '}
{new Date(earning.created_at).toLocaleDateString()}
</div> </div>
</div> </div>
<div className="text-success-400 font-semibold"> <div className='text-success-400 font-semibold'>
{formatPositive(earning.amount_rubles)} {formatPositive(earning.amount_rubles)}
</div> </div>
</div> </div>