From 793dc51a9c20c7e5a2fe5ed34f2ff8c9e4a53feb Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 21 Jan 2026 08:13:25 +0300 Subject: [PATCH 1/3] Update promo.ts --- src/api/promo.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/api/promo.ts b/src/api/promo.ts index ba0a1e8..9512378 100644 --- a/src/api/promo.ts +++ b/src/api/promo.ts @@ -34,6 +34,27 @@ export interface ClaimOfferResponse { expires_at: string | null } +export interface LoyaltyTierInfo { + id: number + name: string + threshold_rubles: number + server_discount_percent: number + traffic_discount_percent: number + device_discount_percent: number + period_discounts: Record + is_current: boolean + is_achieved: boolean +} + +export interface LoyaltyTiersResponse { + tiers: LoyaltyTierInfo[] + current_spent_rubles: number + current_tier_name: string | null + next_tier_name: string | null + next_tier_threshold_rubles: number | null + progress_percent: number +} + export const promoApi = { // Get available promo offers getOffers: async (): Promise => { @@ -53,6 +74,12 @@ export const promoApi = { return response.data }, + // Get loyalty tiers (promo groups with spending thresholds) + getLoyaltyTiers: async (): Promise => { + const response = await apiClient.get('/cabinet/promo/loyalty-tiers') + return response.data + }, + // Claim a promo offer claimOffer: async (offerId: number): Promise => { const response = await apiClient.post('/cabinet/promo/claim', { From 06d3b8c090351d24bc92472246ab46ac6f39a67c Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 21 Jan 2026 08:14:01 +0300 Subject: [PATCH 2/3] Add files via upload --- src/locales/en.json | 20 +++++++++++++++++++- src/locales/fa.json | 20 +++++++++++++++++++- src/locales/ru.json | 20 +++++++++++++++++++- src/locales/zh.json | 20 +++++++++++++++++++- 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/locales/en.json b/src/locales/en.json index 555bc27..641ecad 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -1269,9 +1269,27 @@ "rules": "Rules", "privacy": "Privacy Policy", "offer": "Public Offer", + "loyalty": "Loyalty", "noFaq": "No FAQ available", "noContent": "No content available", - "updatedAt": "Updated" + "updatedAt": "Updated", + "noLoyaltyTiers": "Loyalty program is not available yet", + "yourProgress": "Your Progress", + "totalSpent": "Total spent", + "currentStatus": "Current status", + "nextStatus": "Next status", + "toNextStatus": "To next status", + "allStatusesAchieved": "You have achieved the maximum status!", + "statusAchieved": "Achieved", + "statusCurrent": "Current", + "statusLocked": "Locked", + "threshold": "Threshold", + "discounts": "Discounts", + "serverDiscount": "Servers", + "trafficDiscount": "Traffic", + "deviceDiscount": "Devices", + "periodDiscount": "{{days}} days", + "noDiscounts": "No discounts" }, "onboarding": { "skip": "Skip", diff --git a/src/locales/fa.json b/src/locales/fa.json index 94c6436..ea11732 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -755,9 +755,27 @@ "rules": "قوانین", "privacy": "حریم خصوصی", "offer": "شرایط خدمات", + "loyalty": "سطح وفاداری", "noFaq": "سوال متداولی موجود نیست", "noContent": "محتوایی موجود نیست", - "updatedAt": "به‌روزرسانی" + "updatedAt": "به‌روزرسانی", + "noLoyaltyTiers": "برنامه وفاداری هنوز در دسترس نیست", + "yourProgress": "پیشرفت شما", + "totalSpent": "کل هزینه شده", + "currentStatus": "وضعیت فعلی", + "nextStatus": "وضعیت بعدی", + "toNextStatus": "تا وضعیت بعدی", + "allStatusesAchieved": "شما به بالاترین سطح رسیده‌اید!", + "statusAchieved": "دستیافته", + "statusCurrent": "فعلی", + "statusLocked": "قفل شده", + "threshold": "آستانه", + "discounts": "تخفیف‌ها", + "serverDiscount": "سرورها", + "trafficDiscount": "ترافیک", + "deviceDiscount": "دستگاه‌ها", + "periodDiscount": "{{days}} روز", + "noDiscounts": "بدون تخفیف" }, "onboarding": { "skip": "رد کردن", diff --git a/src/locales/ru.json b/src/locales/ru.json index c156fc6..3fc441d 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -1962,9 +1962,27 @@ "rules": "Правила", "privacy": "Конфиденциальность", "offer": "Оферта", + "loyalty": "Статусы", "noFaq": "Нет вопросов и ответов", "noContent": "Нет контента", - "updatedAt": "Обновлено" + "updatedAt": "Обновлено", + "noLoyaltyTiers": "Программа лояльности пока недоступна", + "yourProgress": "Ваш прогресс", + "totalSpent": "Всего потрачено", + "currentStatus": "Текущий статус", + "nextStatus": "Следующий статус", + "toNextStatus": "До следующего статуса", + "allStatusesAchieved": "Вы достигли максимального статуса!", + "statusAchieved": "Достигнут", + "statusCurrent": "Текущий", + "statusLocked": "Недоступен", + "threshold": "Порог", + "discounts": "Скидки", + "serverDiscount": "На серверы", + "trafficDiscount": "На трафик", + "deviceDiscount": "На устройства", + "periodDiscount": "За {{days}} дн.", + "noDiscounts": "Нет скидок" }, "onboarding": { "skip": "Пропустить", diff --git a/src/locales/zh.json b/src/locales/zh.json index 1dbf6f1..9f8bf6e 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -756,9 +756,27 @@ "rules": "规则", "privacy": "隐私政策", "offer": "服务条款", + "loyalty": "会员等级", "noFaq": "暂无常见问题", "noContent": "暂无内容", - "updatedAt": "更新时间" + "updatedAt": "更新时间", + "noLoyaltyTiers": "会员计划暂不可用", + "yourProgress": "您的进度", + "totalSpent": "总消费", + "currentStatus": "当前等级", + "nextStatus": "下一等级", + "toNextStatus": "距离下一等级", + "allStatusesAchieved": "您已达到最高等级!", + "statusAchieved": "已达成", + "statusCurrent": "当前", + "statusLocked": "未解锁", + "threshold": "门槛", + "discounts": "折扣", + "serverDiscount": "服务器", + "trafficDiscount": "流量", + "deviceDiscount": "设备", + "periodDiscount": "{{days}}天", + "noDiscounts": "无折扣" }, "onboarding": { "skip": "跳过", From 71b1c159bf02624337436c8f27d55a87775249cd Mon Sep 17 00:00:00 2001 From: Egor Date: Wed, 21 Jan 2026 08:14:28 +0300 Subject: [PATCH 3/3] Update Info.tsx --- src/pages/Info.tsx | 193 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 192 insertions(+), 1 deletion(-) diff --git a/src/pages/Info.tsx b/src/pages/Info.tsx index 1ba1e16..1e9f7e5 100644 --- a/src/pages/Info.tsx +++ b/src/pages/Info.tsx @@ -3,6 +3,7 @@ import { useQuery } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import DOMPurify from 'dompurify' import { infoApi, FaqPage } from '../api/info' +import { promoApi, LoyaltyTierInfo } from '../api/promo' const InfoIcon = () => ( @@ -28,6 +29,12 @@ const ShieldIcon = () => ( ) +const StarIcon = () => ( + + + +) + const ChevronIcon = ({ expanded }: { expanded: boolean }) => ( ( ) -type TabType = 'faq' | 'rules' | 'privacy' | 'offer' +type TabType = 'faq' | 'rules' | 'privacy' | 'offer' | 'loyalty' // Sanitize HTML content to prevent XSS const sanitizeHtml = (html: string): string => { @@ -134,11 +141,20 @@ export default function Info() { refetchOnMount: 'always', }) + const { data: loyaltyData, isLoading: loyaltyLoading } = useQuery({ + queryKey: ['loyalty-tiers'], + queryFn: promoApi.getLoyaltyTiers, + enabled: activeTab === 'loyalty', + staleTime: 0, + refetchOnMount: 'always', + }) + const tabs = [ { id: 'faq' as TabType, label: t('info.faq'), icon: QuestionIcon }, { id: 'rules' as TabType, label: t('info.rules'), icon: DocumentIcon }, { id: 'privacy' as TabType, label: t('info.privacy'), icon: ShieldIcon }, { id: 'offer' as TabType, label: t('info.offer'), icon: DocumentIcon }, + { id: 'loyalty' as TabType, label: t('info.loyalty'), icon: StarIcon }, ] const toggleFaq = (id: number) => { @@ -272,6 +288,181 @@ export default function Info() { ) } + if (activeTab === 'loyalty') { + if (loyaltyLoading) { + return ( +
+
+
+ ) + } + + if (!loyaltyData || loyaltyData.tiers.length === 0) { + return ( +
+ {t('info.noLoyaltyTiers')} +
+ ) + } + + 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 ( + + {t('info.statusCurrent')} + + ) + } + if (tier.is_achieved) { + return ( + + {t('info.statusAchieved')} + + ) + } + return ( + + {t('info.statusLocked')} + + ) + } + + 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 ( +
+ {/* Progress Card */} +
+

{t('info.yourProgress')}

+ +
+
+
{t('info.totalSpent')}
+
+ {formatCurrency(loyaltyData.current_spent_rubles)} +
+
+
+
{t('info.currentStatus')}
+
+ {loyaltyData.current_tier_name || '-'} +
+
+
+ + {/* Progress bar to next tier */} + {loyaltyData.next_tier_name && loyaltyData.next_tier_threshold_rubles ? ( +
+
+ {t('info.nextStatus')}: {loyaltyData.next_tier_name} + {t('info.toNextStatus')}: {formatCurrency(loyaltyData.next_tier_threshold_rubles - loyaltyData.current_spent_rubles)} +
+
+
+
+
+ {loyaltyData.progress_percent.toFixed(1)}% +
+
+ ) : ( +
+ {t('info.allStatusesAchieved')} +
+ )} +
+ + {/* Tiers List */} +
+ {loyaltyData.tiers.map((tier) => ( +
+
+
+
+ +
+
+

{tier.name}

+

+ {t('info.threshold')}: {formatCurrency(tier.threshold_rubles)} +

+
+
+ {getStatusBadge(tier)} +
+ + {/* Discounts */} + {hasAnyDiscount(tier) ? ( +
+
{t('info.discounts')}:
+
+ {tier.server_discount_percent > 0 && ( + + {t('info.serverDiscount')}: -{tier.server_discount_percent}% + + )} + {tier.traffic_discount_percent > 0 && ( + + {t('info.trafficDiscount')}: -{tier.traffic_discount_percent}% + + )} + {tier.device_discount_percent > 0 && ( + + {t('info.deviceDiscount')}: -{tier.device_discount_percent}% + + )} + {Object.entries(tier.period_discounts).map(([days, percent]) => ( + + {t('info.periodDiscount', { days })}: -{percent}% + + ))} +
+
+ ) : ( +
{t('info.noDiscounts')}
+ )} +
+ ))} +
+
+ ) + } + return null }