Update Info.tsx

This commit is contained in:
Egor
2026-01-21 08:14:28 +03:00
committed by GitHub
parent 06d3b8c090
commit 71b1c159bf

View File

@@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import DOMPurify from 'dompurify' import DOMPurify from 'dompurify'
import { infoApi, FaqPage } from '../api/info' import { infoApi, FaqPage } from '../api/info'
import { promoApi, LoyaltyTierInfo } from '../api/promo'
const InfoIcon = () => ( const InfoIcon = () => (
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}> <svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
@@ -28,6 +29,12 @@ const ShieldIcon = () => (
</svg> </svg>
) )
const StarIcon = () => (
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path strokeLinecap="round" strokeLinejoin="round" d="M11.48 3.499a.562.562 0 011.04 0l2.125 5.111a.563.563 0 00.475.345l5.518.442c.499.04.701.663.321.988l-4.204 3.602a.563.563 0 00-.182.557l1.285 5.385a.562.562 0 01-.84.61l-4.725-2.885a.563.563 0 00-.586 0L6.982 20.54a.562.562 0 01-.84-.61l1.285-5.386a.562.562 0 00-.182-.557l-4.204-3.602a.563.563 0 01.321-.988l5.518-.442a.563.563 0 00.475-.345L11.48 3.5z" />
</svg>
)
const ChevronIcon = ({ expanded }: { expanded: boolean }) => ( const ChevronIcon = ({ expanded }: { expanded: boolean }) => (
<svg <svg
className={`w-5 h-5 transition-transform ${expanded ? 'rotate-180' : ''}`} className={`w-5 h-5 transition-transform ${expanded ? 'rotate-180' : ''}`}
@@ -40,7 +47,7 @@ const ChevronIcon = ({ expanded }: { expanded: boolean }) => (
</svg> </svg>
) )
type TabType = 'faq' | 'rules' | 'privacy' | 'offer' type TabType = 'faq' | 'rules' | 'privacy' | 'offer' | 'loyalty'
// Sanitize HTML content to prevent XSS // Sanitize HTML content to prevent XSS
const sanitizeHtml = (html: string): string => { const sanitizeHtml = (html: string): string => {
@@ -134,11 +141,20 @@ export default function Info() {
refetchOnMount: 'always', refetchOnMount: 'always',
}) })
const { data: loyaltyData, isLoading: loyaltyLoading } = useQuery({
queryKey: ['loyalty-tiers'],
queryFn: promoApi.getLoyaltyTiers,
enabled: activeTab === 'loyalty',
staleTime: 0,
refetchOnMount: 'always',
})
const tabs = [ const tabs = [
{ id: 'faq' as TabType, label: t('info.faq'), icon: QuestionIcon }, { id: 'faq' as TabType, label: t('info.faq'), icon: QuestionIcon },
{ id: 'rules' as TabType, label: t('info.rules'), icon: DocumentIcon }, { id: 'rules' as TabType, label: t('info.rules'), icon: DocumentIcon },
{ id: 'privacy' as TabType, label: t('info.privacy'), icon: ShieldIcon }, { id: 'privacy' as TabType, label: t('info.privacy'), icon: ShieldIcon },
{ id: 'offer' as TabType, label: t('info.offer'), icon: DocumentIcon }, { id: 'offer' as TabType, label: t('info.offer'), icon: DocumentIcon },
{ id: 'loyalty' as TabType, label: t('info.loyalty'), icon: StarIcon },
] ]
const toggleFaq = (id: number) => { const toggleFaq = (id: number) => {
@@ -272,6 +288,181 @@ export default function Info() {
) )
} }
if (activeTab === 'loyalty') {
if (loyaltyLoading) {
return (
<div className="flex justify-center py-8">
<div className="w-8 h-8 border-2 border-accent-500 border-t-transparent rounded-full animate-spin" />
</div>
)
}
if (!loyaltyData || loyaltyData.tiers.length === 0) {
return (
<div className="text-center py-8 text-dark-400">
{t('info.noLoyaltyTiers')}
</div>
)
}
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('ru-RU', {
style: 'currency',
currency: 'RUB',
minimumFractionDigits: 0,
maximumFractionDigits: 0,
}).format(amount)
}
const getStatusBadge = (tier: LoyaltyTierInfo) => {
if (tier.is_current) {
return (
<span className="px-2 py-1 text-xs font-medium bg-accent-500/20 text-accent-400 rounded-full">
{t('info.statusCurrent')}
</span>
)
}
if (tier.is_achieved) {
return (
<span className="px-2 py-1 text-xs font-medium bg-success-500/20 text-success-400 rounded-full">
{t('info.statusAchieved')}
</span>
)
}
return (
<span className="px-2 py-1 text-xs font-medium bg-dark-600 text-dark-400 rounded-full">
{t('info.statusLocked')}
</span>
)
}
const hasAnyDiscount = (tier: LoyaltyTierInfo) => {
return (
tier.server_discount_percent > 0 ||
tier.traffic_discount_percent > 0 ||
tier.device_discount_percent > 0 ||
Object.keys(tier.period_discounts).length > 0
)
}
return (
<div className="space-y-6">
{/* Progress Card */}
<div className="bento-card p-5">
<h3 className="text-lg font-semibold text-dark-50 mb-4">{t('info.yourProgress')}</h3>
<div className="grid grid-cols-2 gap-4 mb-4">
<div className="bg-dark-800/50 rounded-xl p-3">
<div className="text-xs text-dark-400 mb-1">{t('info.totalSpent')}</div>
<div className="text-lg font-bold text-dark-50">
{formatCurrency(loyaltyData.current_spent_rubles)}
</div>
</div>
<div className="bg-dark-800/50 rounded-xl p-3">
<div className="text-xs text-dark-400 mb-1">{t('info.currentStatus')}</div>
<div className="text-lg font-bold text-accent-400">
{loyaltyData.current_tier_name || '-'}
</div>
</div>
</div>
{/* Progress bar to next tier */}
{loyaltyData.next_tier_name && loyaltyData.next_tier_threshold_rubles ? (
<div>
<div className="flex justify-between text-xs text-dark-400 mb-2">
<span>{t('info.nextStatus')}: {loyaltyData.next_tier_name}</span>
<span>{t('info.toNextStatus')}: {formatCurrency(loyaltyData.next_tier_threshold_rubles - loyaltyData.current_spent_rubles)}</span>
</div>
<div className="h-3 bg-dark-700 rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-accent-500 to-accent-400 rounded-full transition-all duration-500"
style={{ width: `${Math.min(100, loyaltyData.progress_percent)}%` }}
/>
</div>
<div className="text-right text-xs text-dark-400 mt-1">
{loyaltyData.progress_percent.toFixed(1)}%
</div>
</div>
) : (
<div className="text-center py-2 text-success-400 font-medium">
{t('info.allStatusesAchieved')}
</div>
)}
</div>
{/* Tiers List */}
<div className="space-y-3">
{loyaltyData.tiers.map((tier) => (
<div
key={tier.id}
className={`bento-card p-4 transition-all ${
tier.is_current
? 'ring-2 ring-accent-500/50 bg-accent-500/5'
: tier.is_achieved
? 'bg-success-500/5'
: 'opacity-70'
}`}
>
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<div
className={`w-10 h-10 rounded-xl flex items-center justify-center ${
tier.is_current
? 'bg-accent-500/20 text-accent-400'
: tier.is_achieved
? 'bg-success-500/20 text-success-400'
: 'bg-dark-700 text-dark-400'
}`}
>
<StarIcon />
</div>
<div>
<h4 className="font-semibold text-dark-50">{tier.name}</h4>
<p className="text-xs text-dark-400">
{t('info.threshold')}: {formatCurrency(tier.threshold_rubles)}
</p>
</div>
</div>
{getStatusBadge(tier)}
</div>
{/* Discounts */}
{hasAnyDiscount(tier) ? (
<div className="bg-dark-800/50 rounded-xl p-3">
<div className="text-xs text-dark-400 mb-2">{t('info.discounts')}:</div>
<div className="flex flex-wrap gap-2">
{tier.server_discount_percent > 0 && (
<span className="px-2 py-1 text-xs bg-dark-700 rounded-lg text-dark-200">
{t('info.serverDiscount')}: -{tier.server_discount_percent}%
</span>
)}
{tier.traffic_discount_percent > 0 && (
<span className="px-2 py-1 text-xs bg-dark-700 rounded-lg text-dark-200">
{t('info.trafficDiscount')}: -{tier.traffic_discount_percent}%
</span>
)}
{tier.device_discount_percent > 0 && (
<span className="px-2 py-1 text-xs bg-dark-700 rounded-lg text-dark-200">
{t('info.deviceDiscount')}: -{tier.device_discount_percent}%
</span>
)}
{Object.entries(tier.period_discounts).map(([days, percent]) => (
<span key={days} className="px-2 py-1 text-xs bg-dark-700 rounded-lg text-dark-200">
{t('info.periodDiscount', { days })}: -{percent}%
</span>
))}
</div>
</div>
) : (
<div className="text-xs text-dark-500 italic">{t('info.noDiscounts')}</div>
)}
</div>
))}
</div>
</div>
)
}
return null return null
} }