feat: add gift navigation, routes, and i18n translations

This commit is contained in:
Fringg
2026-03-09 19:04:50 +03:00
parent 814b1f5e96
commit 7890d480e0
8 changed files with 249 additions and 22 deletions

View File

@@ -36,6 +36,8 @@ const Contests = lazy(() => import('./pages/Contests'));
const Polls = lazy(() => import('./pages/Polls')); const Polls = lazy(() => import('./pages/Polls'));
const Info = lazy(() => import('./pages/Info')); const Info = lazy(() => import('./pages/Info'));
const Wheel = lazy(() => import('./pages/Wheel')); const Wheel = lazy(() => import('./pages/Wheel'));
const GiftSubscription = lazy(() => import('./pages/GiftSubscription'));
const GiftResult = lazy(() => import('./pages/GiftResult'));
const Connection = lazy(() => import('./pages/Connection')); const Connection = lazy(() => import('./pages/Connection'));
const ConnectionQR = lazy(() => import('./pages/ConnectionQR')); const ConnectionQR = lazy(() => import('./pages/ConnectionQR'));
const QuickPurchase = lazy(() => import('./pages/QuickPurchase')); const QuickPurchase = lazy(() => import('./pages/QuickPurchase'));
@@ -420,6 +422,26 @@ function App() {
</ProtectedRoute> </ProtectedRoute>
} }
/> />
<Route
path="/gift"
element={
<ProtectedRoute>
<LazyPage>
<GiftSubscription />
</LazyPage>
</ProtectedRoute>
}
/>
<Route
path="/gift/result"
element={
<ProtectedRoute>
<LazyPage>
<GiftResult />
</LazyPage>
</ProtectedRoute>
}
/>
<Route <Route
path="/connection/qr" path="/connection/qr"
element={ element={

View File

@@ -35,6 +35,7 @@ import {
InfoIcon, InfoIcon,
CogIcon, CogIcon,
WheelIcon, WheelIcon,
GiftIcon,
MenuIcon, MenuIcon,
CloseIcon, CloseIcon,
SunIcon, SunIcon,
@@ -60,6 +61,7 @@ interface AppHeaderProps {
referralEnabled?: boolean; referralEnabled?: boolean;
hasContests?: boolean; hasContests?: boolean;
hasPolls?: boolean; hasPolls?: boolean;
giftEnabled?: boolean;
} }
export function AppHeader({ export function AppHeader({
@@ -75,6 +77,7 @@ export function AppHeader({
referralEnabled, referralEnabled,
hasContests, hasContests,
hasPolls, hasPolls,
giftEnabled,
}: AppHeaderProps) { }: AppHeaderProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const location = useLocation(); const location = useLocation();
@@ -164,6 +167,7 @@ export function AppHeader({
...(hasContests ? [{ path: '/contests', label: t('nav.contests'), icon: GamepadIcon }] : []), ...(hasContests ? [{ path: '/contests', label: t('nav.contests'), icon: GamepadIcon }] : []),
...(hasPolls ? [{ path: '/polls', label: t('nav.polls'), icon: ClipboardIcon }] : []), ...(hasPolls ? [{ path: '/polls', label: t('nav.polls'), icon: ClipboardIcon }] : []),
...(wheelEnabled ? [{ path: '/wheel', label: t('nav.wheel'), icon: WheelIcon }] : []), ...(wheelEnabled ? [{ path: '/wheel', label: t('nav.wheel'), icon: WheelIcon }] : []),
...(giftEnabled ? [{ path: '/gift', label: t('nav.gift'), icon: GiftIcon }] : []),
{ path: '/info', label: t('nav.info'), icon: InfoIcon }, { path: '/info', label: t('nav.info'), icon: InfoIcon },
]; ];

View File

@@ -20,7 +20,7 @@ import CampaignBonusNotifier from '@/components/CampaignBonusNotifier';
import SuccessNotificationModal from '@/components/SuccessNotificationModal'; import SuccessNotificationModal from '@/components/SuccessNotificationModal';
import LanguageSwitcher from '@/components/LanguageSwitcher'; import LanguageSwitcher from '@/components/LanguageSwitcher';
import TicketNotificationBell from '@/components/TicketNotificationBell'; import TicketNotificationBell from '@/components/TicketNotificationBell';
import { SubscriptionIcon } from '@/components/icons'; import { SubscriptionIcon, GiftIcon } from '@/components/icons';
import { MobileBottomNav } from './MobileBottomNav'; import { MobileBottomNav } from './MobileBottomNav';
import { AppHeader } from './AppHeader'; import { AppHeader } from './AppHeader';
@@ -203,7 +203,7 @@ export function AppShell({ children }: AppShellProps) {
// Extracted hooks // Extracted hooks
const { appName, logoLetter, hasCustomLogo, logoUrl } = useBranding(); const { appName, logoLetter, hasCustomLogo, logoUrl } = useBranding();
const { referralEnabled, wheelEnabled, hasContests, hasPolls } = useFeatureFlags(); const { referralEnabled, wheelEnabled, hasContests, hasPolls, giftEnabled } = useFeatureFlags();
useScrollRestoration(); useScrollRestoration();
// Theme toggle visibility // Theme toggle visibility
@@ -349,6 +349,21 @@ export function AppShell({ children }: AppShellProps) {
<span>{t('nav.referral')}</span> <span>{t('nav.referral')}</span>
</Link> </Link>
)} )}
{giftEnabled && (
<Link
to="/gift"
onClick={handleNavClick}
className={cn(
'flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
isActive('/gift')
? 'bg-dark-800 text-dark-50'
: 'text-dark-400 hover:bg-dark-800/50 hover:text-dark-200',
)}
>
<GiftIcon className="h-4 w-4" />
<span>{t('nav.gift')}</span>
</Link>
)}
{isAdmin && ( {isAdmin && (
<> <>
{/* Separator before admin */} {/* Separator before admin */}
@@ -415,6 +430,7 @@ export function AppShell({ children }: AppShellProps) {
referralEnabled={referralEnabled} referralEnabled={referralEnabled}
hasContests={hasContests} hasContests={hasContests}
hasPolls={hasPolls} hasPolls={hasPolls}
giftEnabled={giftEnabled}
/> />
{/* Desktop spacer */} {/* Desktop spacer */}

View File

@@ -16,6 +16,7 @@ export {
InfoIcon, InfoIcon,
CogIcon, CogIcon,
WheelIcon, WheelIcon,
GiftIcon,
SearchIcon, SearchIcon,
PlusIcon, PlusIcon,
ArrowRightIcon, ArrowRightIcon,

View File

@@ -44,7 +44,8 @@
"polls": "Polls", "polls": "Polls",
"info": "Info", "info": "Info",
"wheel": "Fortune Wheel", "wheel": "Fortune Wheel",
"menu": "Menu" "menu": "Menu",
"gift": "Gift"
}, },
"notifications": { "notifications": {
"ticketNotifications": "Ticket Notifications", "ticketNotifications": "Ticket Notifications",
@@ -3293,7 +3294,7 @@
"status_failed": "Failed", "status_failed": "Failed",
"status_expired": "Expired", "status_expired": "Expired",
"noPurchases": "No purchases", "noPurchases": "No purchases",
"showing": "Showing {{from}}\u2013{{to}} of {{total}}", "showing": "Showing {{from}}{{to}} of {{total}}",
"page": "Page {{current}} of {{total}}", "page": "Page {{current}} of {{total}}",
"prev": "Previous", "prev": "Previous",
"next": "Next" "next": "Next"
@@ -4114,5 +4115,50 @@
"nMonths_one": "{{count}} month", "nMonths_one": "{{count}} month",
"nMonths_other": "{{count}} months" "nMonths_other": "{{count}} months"
} }
},
"gift": {
"title": "Gift Subscription",
"subtitle": "Send a VPN subscription as a gift",
"choosePeriod": "Choose period",
"chooseTariff": "Choose tariff",
"recipient": "Recipient",
"recipientPlaceholder": "Email or @telegram",
"recipientHint": "Enter email or Telegram username",
"giftMessage": "Greeting",
"giftMessagePlaceholder": "Add a personal message (optional)",
"paymentMode": "Payment method",
"fromBalance": "From balance",
"viaGateway": "Via payment gateway",
"yourBalance": "Your balance",
"insufficientBalance": "Insufficient funds",
"topUpBalance": "Top up balance",
"total": "Total",
"giftButton": "Send Gift",
"sending": "Sending gift...",
"gb": "GB",
"devices": "devices",
"paymentMethod": "Payment method",
"processing": "Processing...",
"successTitle": "Gift sent!",
"successDesc": "Recipient {{contact}} will be notified",
"pendingTitle": "Awaiting payment",
"pendingDesc": "Complete the payment in the payment system",
"pendingActivationTitle": "Pending activation",
"pendingActivationDesc": "Recipient has an active subscription. Gift is pending activation.",
"failedTitle": "Error",
"failedDesc": "Failed to send gift. Please try again.",
"backToGift": "Go back",
"backToDashboard": "Back to dashboard",
"tryAgain": "Try again",
"pollTimeout": "Processing is taking longer than usual",
"pollTimeoutDesc": "Try checking the status later",
"retry": "Check again",
"noToken": "Invalid link",
"tariff": "Tariff",
"period": "Period",
"giftMessageLabel": "Message",
"recipientLabel": "Recipient",
"featureDisabled": "Gift feature is temporarily unavailable",
"redirecting": "Redirecting..."
} }
} }

View File

@@ -44,7 +44,8 @@
"polls": "نظرسنجی", "polls": "نظرسنجی",
"info": "اطلاعات", "info": "اطلاعات",
"wheel": "چرخ شانس", "wheel": "چرخ شانس",
"menu": "منو" "menu": "منو",
"gift": "هدیه"
}, },
"notifications": { "notifications": {
"ticketNotifications": "اعلان‌های تیکت", "ticketNotifications": "اعلان‌های تیکت",
@@ -2964,10 +2965,10 @@
"content": "محتوا", "content": "محتوا",
"save": "ذخیره", "save": "ذخیره",
"back": "بازگشت", "back": "بازگشت",
"invalidSlug": "Slug \u0641\u0642\u0637 \u0645\u06cc\u200c\u062a\u0648\u0627\u0646\u062f \u0634\u0627\u0645\u0644 \u062d\u0631\u0648\u0641 \u06a9\u0648\u0686\u06a9\u060c \u0627\u0639\u062f\u0627\u062f \u0648 \u062e\u0637 \u062a\u06cc\u0631\u0647 \u0628\u0627\u0634\u062f", "invalidSlug": "Slug فقط می‌تواند شامل حروف کوچک، اعداد و خط تیره باشد",
"titleRequired": "\u0639\u0646\u0648\u0627\u0646 \u0627\u0644\u0632\u0627\u0645\u06cc \u0627\u0633\u062a", "titleRequired": "عنوان الزامی است",
"noTariffs": "\u062d\u062f\u0627\u0642\u0644 \u06cc\u06a9 \u062a\u0639\u0631\u0641\u0647 \u0627\u0646\u062a\u062e\u0627\u0628 \u06a9\u0646\u06cc\u062f", "noTariffs": "حداقل یک تعرفه انتخاب کنید",
"noPaymentMethods": "\u062d\u062f\u0627\u0642\u0644 \u06cc\u06a9 \u0631\u0648\u0634 \u067e\u0631\u062f\u0627\u062e\u062a \u0627\u0636\u0627\u0641\u0647 \u06a9\u0646\u06cc\u062f", "noPaymentMethods": "حداقل یک روش پرداخت اضافه کنید",
"selectMethods": "انتخاب روش‌های پرداخت موجود", "selectMethods": "انتخاب روش‌های پرداخت موجود",
"noSystemMethods": "هیچ روش پرداختی در سیستم پیکربندی نشده است", "noSystemMethods": "هیچ روش پرداختی در سیستم پیکربندی نشده است",
"methodOrder": "برای تغییر ترتیب بکشید", "methodOrder": "برای تغییر ترتیب بکشید",
@@ -3568,9 +3569,9 @@
"copy": "کپی", "copy": "کپی",
"copied": "کپی شد!", "copied": "کپی شد!",
"copyLink": "کپی لینک", "copyLink": "کپی لینک",
"giftToggleLabel": "\u0646\u0648\u0639 \u062e\u0631\u06cc\u062f", "giftToggleLabel": "نوع خرید",
"pollTimedOut": "\u0632\u0645\u0627\u0646 \u0628\u06cc\u0634\u062a\u0631\u06cc \u0637\u0648\u0644 \u06a9\u0634\u06cc\u062f", "pollTimedOut": "زمان بیشتری طول کشید",
"pollTimedOutDesc": "\u067e\u0631\u062f\u0627\u0632\u0634 \u067e\u0631\u062f\u0627\u062e\u062a \u0628\u06cc\u0634\u062a\u0631 \u0627\u0632 \u062d\u062f \u0645\u0639\u0645\u0648\u0644 \u0637\u0648\u0644 \u06a9\u0634\u06cc\u062f\u0647 \u0627\u0633\u062a. \u0645\u06cc\u200c\u062a\u0648\u0627\u0646\u06cc\u062f \u062f\u0648\u0628\u0627\u0631\u0647 \u0628\u0631\u0631\u0633\u06cc \u06a9\u0646\u06cc\u062f.", "pollTimedOutDesc": "پردازش پرداخت بیشتر از حد معمول طول کشیده است. می‌توانید دوباره بررسی کنید.",
"pendingActivation": "اشتراک آماده فعال‌سازی", "pendingActivation": "اشتراک آماده فعال‌سازی",
"pendingActivationDesc": "شما قبلاً اشتراک فعالی دارید. فعال‌سازی اشتراک جدید جایگزین اشتراک فعلی می‌شود.", "pendingActivationDesc": "شما قبلاً اشتراک فعالی دارید. فعال‌سازی اشتراک جدید جایگزین اشتراک فعلی می‌شود.",
"activateNow": "فعال‌سازی", "activateNow": "فعال‌سازی",
@@ -3609,5 +3610,50 @@
"nDays": "{{count}} روز", "nDays": "{{count}} روز",
"nMonths": "{{count}} ماه" "nMonths": "{{count}} ماه"
} }
},
"gift": {
"title": "هدیه اشتراک",
"subtitle": "اشتراک VPN را به عنوان هدیه ارسال کنید",
"choosePeriod": "انتخاب مدت",
"chooseTariff": "انتخاب طرح",
"recipient": "گیرنده",
"recipientPlaceholder": "ایمیل یا @telegram",
"recipientHint": "ایمیل یا نام کاربری تلگرام را وارد کنید",
"giftMessage": "پیام تبریک",
"giftMessagePlaceholder": "پیام شخصی اضافه کنید (اختیاری)",
"paymentMode": "روش پرداخت",
"fromBalance": "از موجودی",
"viaGateway": "درگاه پرداخت",
"yourBalance": "موجودی شما",
"insufficientBalance": "موجودی ناکافی",
"topUpBalance": "شارژ موجودی",
"total": "جمع",
"giftButton": "ارسال هدیه",
"sending": "در حال ارسال هدیه...",
"gb": "گیگابایت",
"devices": "دستگاه",
"paymentMethod": "روش پرداخت",
"processing": "در حال پردازش...",
"successTitle": "هدیه ارسال شد!",
"successDesc": "گیرنده {{contact}} اطلاع‌رسانی خواهد شد",
"pendingTitle": "در انتظار پرداخت",
"pendingDesc": "پرداخت را در سیستم پرداخت تکمیل کنید",
"pendingActivationTitle": "در انتظار فعال‌سازی",
"pendingActivationDesc": "گیرنده اشتراک فعال دارد. هدیه منتظر فعال‌سازی است.",
"failedTitle": "خطا",
"failedDesc": "ارسال هدیه ناموفق بود. لطفاً دوباره تلاش کنید.",
"backToGift": "بازگشت",
"backToDashboard": "بازگشت به داشبورد",
"tryAgain": "تلاش مجدد",
"pollTimeout": "پردازش بیش از حد معمول طول کشیده",
"pollTimeoutDesc": "بعداً وضعیت را بررسی کنید",
"retry": "بررسی مجدد",
"noToken": "لینک نامعتبر",
"tariff": "طرح",
"period": "مدت",
"giftMessageLabel": "پیام",
"recipientLabel": "گیرنده",
"featureDisabled": "قابلیت هدیه موقتاً در دسترس نیست",
"redirecting": "در حال انتقال..."
} }
} }

View File

@@ -44,7 +44,8 @@
"polls": "Опросы", "polls": "Опросы",
"info": "Информация", "info": "Информация",
"wheel": "Колесо удачи", "wheel": "Колесо удачи",
"menu": "Меню" "menu": "Меню",
"gift": "Подарить"
}, },
"notifications": { "notifications": {
"ticketNotifications": "Уведомления о тикетах", "ticketNotifications": "Уведомления о тикетах",
@@ -3845,7 +3846,7 @@
"status_failed": "Ошибка", "status_failed": "Ошибка",
"status_expired": "Истёк", "status_expired": "Истёк",
"noPurchases": "Нет покупок", "noPurchases": "Нет покупок",
"showing": "Показано {{from}}\u2013{{to}} из {{total}}", "showing": "Показано {{from}}{{to}} из {{total}}",
"page": "Стр. {{current}} из {{total}}", "page": "Стр. {{current}} из {{total}}",
"prev": "Назад", "prev": "Назад",
"next": "Далее" "next": "Далее"
@@ -4676,5 +4677,50 @@
"nMonths_few": "{{count}} месяца", "nMonths_few": "{{count}} месяца",
"nMonths_many": "{{count}} месяцев" "nMonths_many": "{{count}} месяцев"
} }
},
"gift": {
"title": "Подарить подписку",
"subtitle": "Отправьте VPN-подписку в подарок",
"choosePeriod": "Выберите период",
"chooseTariff": "Выберите тариф",
"recipient": "Получатель",
"recipientPlaceholder": "Email или @telegram",
"recipientHint": "Введите email или юзернейм в Telegram",
"giftMessage": "Поздравление",
"giftMessagePlaceholder": "Добавьте личное сообщение (необязательно)",
"paymentMode": "Способ оплаты",
"fromBalance": "С баланса",
"viaGateway": "Через платёжку",
"yourBalance": "Ваш баланс",
"insufficientBalance": "Недостаточно средств",
"topUpBalance": "Пополнить баланс",
"total": "Итого",
"giftButton": "Подарить",
"sending": "Отправляем подарок...",
"gb": "ГБ",
"devices": "устройств",
"paymentMethod": "Способ оплаты",
"processing": "Обработка...",
"successTitle": "Подарок отправлен!",
"successDesc": "Получатель {{contact}} получит уведомление",
"pendingTitle": "Ожидание оплаты",
"pendingDesc": "Завершите оплату в платёжной системе",
"pendingActivationTitle": "Ожидает активации",
"pendingActivationDesc": "У получателя есть активная подписка. Подарок ожидает активации.",
"failedTitle": "Ошибка",
"failedDesc": "Не удалось отправить подарок. Попробуйте снова.",
"backToGift": "Вернуться",
"backToDashboard": "На главную",
"tryAgain": "Попробовать снова",
"pollTimeout": "Обработка занимает больше времени, чем обычно",
"pollTimeoutDesc": "Попробуйте проверить статус позже",
"retry": "Проверить снова",
"noToken": "Ссылка недействительна",
"tariff": "Тариф",
"period": "Период",
"giftMessageLabel": "Сообщение",
"recipientLabel": "Получатель",
"featureDisabled": "Функция подарков временно недоступна",
"redirecting": "Перенаправляем..."
} }
} }

View File

@@ -44,7 +44,8 @@
"polls": "问卷", "polls": "问卷",
"info": "信息", "info": "信息",
"wheel": "幸运转盘", "wheel": "幸运转盘",
"menu": "菜单" "menu": "菜单",
"gift": "赠送"
}, },
"notifications": { "notifications": {
"ticketNotifications": "工单通知", "ticketNotifications": "工单通知",
@@ -2963,10 +2964,10 @@
"content": "内容", "content": "内容",
"save": "保存", "save": "保存",
"back": "返回", "back": "返回",
"invalidSlug": "Slug\u53ea\u80fd\u5305\u542b\u5c0f\u5199\u5b57\u6bcd\u3001\u6570\u5b57\u548c\u8fde\u5b57\u7b26", "invalidSlug": "Slug只能包含小写字母、数字和连字符",
"titleRequired": "\u6807\u9898\u4e3a\u5fc5\u586b\u9879", "titleRequired": "标题为必填项",
"noTariffs": "\u8bf7\u81f3\u5c11\u9009\u62e9\u4e00\u4e2a\u5957\u9910", "noTariffs": "请至少选择一个套餐",
"noPaymentMethods": "\u8bf7\u81f3\u5c11\u6dfb\u52a0\u4e00\u4e2a\u652f\u4ed8\u65b9\u5f0f", "noPaymentMethods": "请至少添加一个支付方式",
"selectMethods": "选择可用的支付方式", "selectMethods": "选择可用的支付方式",
"noSystemMethods": "系统中未配置任何支付方式", "noSystemMethods": "系统中未配置任何支付方式",
"methodOrder": "拖动以调整顺序", "methodOrder": "拖动以调整顺序",
@@ -3567,9 +3568,9 @@
"copy": "复制", "copy": "复制",
"copied": "已复制!", "copied": "已复制!",
"copyLink": "复制链接", "copyLink": "复制链接",
"giftToggleLabel": "\u8d2d\u4e70\u7c7b\u578b", "giftToggleLabel": "购买类型",
"pollTimedOut": "\u5904\u7406\u65f6\u95f4\u8f83\u957f", "pollTimedOut": "处理时间较长",
"pollTimedOutDesc": "\u4ed8\u6b3e\u5904\u7406\u65f6\u95f4\u6bd4\u5e73\u65f6\u957f\u3002\u60a8\u53ef\u4ee5\u5c1d\u8bd5\u518d\u6b21\u68c0\u67e5\u3002", "pollTimedOutDesc": "付款处理时间比平时长。您可以尝试再次检查。",
"pendingActivation": "订阅准备激活", "pendingActivation": "订阅准备激活",
"pendingActivationDesc": "您已有活跃订阅。激活新订阅将替换当前订阅。", "pendingActivationDesc": "您已有活跃订阅。激活新订阅将替换当前订阅。",
"activateNow": "立即激活", "activateNow": "立即激活",
@@ -3608,5 +3609,50 @@
"nDays": "{{count}}天", "nDays": "{{count}}天",
"nMonths": "{{count}}个月" "nMonths": "{{count}}个月"
} }
},
"gift": {
"title": "赠送订阅",
"subtitle": "将VPN订阅作为礼物发送",
"choosePeriod": "选择时长",
"chooseTariff": "选择套餐",
"recipient": "收件人",
"recipientPlaceholder": "邮箱或 @telegram",
"recipientHint": "输入邮箱或 Telegram 用户名",
"giftMessage": "祝福语",
"giftMessagePlaceholder": "添加个人消息(可选)",
"paymentMode": "支付方式",
"fromBalance": "余额支付",
"viaGateway": "在线支付",
"yourBalance": "您的余额",
"insufficientBalance": "余额不足",
"topUpBalance": "充值",
"total": "合计",
"giftButton": "赠送",
"sending": "正在发送礼物...",
"gb": "GB",
"devices": "设备",
"paymentMethod": "支付方式",
"processing": "处理中...",
"successTitle": "礼物已发送!",
"successDesc": "收件人 {{contact}} 将收到通知",
"pendingTitle": "等待支付",
"pendingDesc": "请在支付系统中完成支付",
"pendingActivationTitle": "等待激活",
"pendingActivationDesc": "收件人有活跃订阅,礼物等待激活。",
"failedTitle": "错误",
"failedDesc": "礼物发送失败,请重试。",
"backToGift": "返回",
"backToDashboard": "返回首页",
"tryAgain": "重试",
"pollTimeout": "处理时间超出预期",
"pollTimeoutDesc": "请稍后再查看状态",
"retry": "再次检查",
"noToken": "无效链接",
"tariff": "套餐",
"period": "时长",
"giftMessageLabel": "消息",
"recipientLabel": "收件人",
"featureDisabled": "礼物功能暂时不可用",
"redirecting": "正在跳转..."
} }
} }