mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
Merge pull request #210 from BEDOLAGA-DEV/dev
Release: admin enhancements, OAuth, connection page, performance improvements
This commit is contained in:
@@ -20,6 +20,9 @@ import { isInTelegramWebApp } from './hooks/useTelegramSDK';
|
|||||||
* Manages Telegram BackButton visibility based on navigation location.
|
* Manages Telegram BackButton visibility based on navigation location.
|
||||||
* Shows back button on non-root routes, hides on root.
|
* Shows back button on non-root routes, hides on root.
|
||||||
*/
|
*/
|
||||||
|
/** Pages reachable from bottom nav — treat as top-level (no back button). */
|
||||||
|
const BOTTOM_NAV_PATHS = ['/', '/subscription', '/balance', '/referral', '/support', '/wheel'];
|
||||||
|
|
||||||
function TelegramBackButton() {
|
function TelegramBackButton() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -27,9 +30,9 @@ function TelegramBackButton() {
|
|||||||
navigateRef.current = navigate;
|
navigateRef.current = navigate;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isRoot = location.pathname === '/' || location.pathname === '';
|
const isTopLevel = location.pathname === '' || BOTTOM_NAV_PATHS.includes(location.pathname);
|
||||||
try {
|
try {
|
||||||
if (isRoot) {
|
if (isTopLevel) {
|
||||||
hideBackButton();
|
hideBackButton();
|
||||||
} else {
|
} else {
|
||||||
showBackButton();
|
showBackButton();
|
||||||
|
|||||||
@@ -208,7 +208,9 @@ export const tariffsApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Delete tariff
|
// Delete tariff
|
||||||
deleteTariff: async (tariffId: number): Promise<{ message: string }> => {
|
deleteTariff: async (
|
||||||
|
tariffId: number,
|
||||||
|
): Promise<{ message: string; affected_subscriptions: number }> => {
|
||||||
const response = await apiClient.delete(`/cabinet/admin/tariffs/${tariffId}`);
|
const response = await apiClient.delete(`/cabinet/admin/tariffs/${tariffId}`);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -355,7 +355,11 @@
|
|||||||
"selectPlatform": "Select platform",
|
"selectPlatform": "Select platform",
|
||||||
"app": "app",
|
"app": "app",
|
||||||
"apps": "apps",
|
"apps": "apps",
|
||||||
"tutorial": "Tutorial"
|
"tutorial": "Tutorial",
|
||||||
|
"notConfigured": "Connection methods are not configured yet",
|
||||||
|
"notConfiguredUser": "Connection setup is in progress. Please check back later.",
|
||||||
|
"notConfiguredAdmin": "No connection apps have been configured. Set them up in the Apps settings.",
|
||||||
|
"goToApps": "Go to Apps settings"
|
||||||
},
|
},
|
||||||
"myDevices": "My Devices",
|
"myDevices": "My Devices",
|
||||||
"noDevices": "No connected devices",
|
"noDevices": "No connected devices",
|
||||||
@@ -1366,6 +1370,7 @@
|
|||||||
"confirmDeleteText": "This action cannot be undone. The tariff will be permanently deleted.",
|
"confirmDeleteText": "This action cannot be undone. The tariff will be permanently deleted.",
|
||||||
"confirmDeleteWithSubscriptions": "This tariff has {{count}} active subscriptions. After deletion, users will need to select a new tariff when renewing.",
|
"confirmDeleteWithSubscriptions": "This tariff has {{count}} active subscriptions. After deletion, users will need to select a new tariff when renewing.",
|
||||||
"deleteSuccess": "Tariff deleted successfully",
|
"deleteSuccess": "Tariff deleted successfully",
|
||||||
|
"deleteSuccessWithSubscriptions": "Tariff deleted. {{count}} subscriptions reset — users will choose a new tariff on renewal.",
|
||||||
"saveOrder": "Save order",
|
"saveOrder": "Save order",
|
||||||
"orderSaved": "Order saved",
|
"orderSaved": "Order saved",
|
||||||
"dragToReorder": "Drag to reorder",
|
"dragToReorder": "Drag to reorder",
|
||||||
|
|||||||
@@ -307,7 +307,11 @@
|
|||||||
"selectPlatform": "انتخاب پلتفرم",
|
"selectPlatform": "انتخاب پلتفرم",
|
||||||
"app": "برنامه",
|
"app": "برنامه",
|
||||||
"apps": "برنامه",
|
"apps": "برنامه",
|
||||||
"tutorial": "آموزش"
|
"tutorial": "آموزش",
|
||||||
|
"notConfigured": "روشهای اتصال هنوز پیکربندی نشدهاند",
|
||||||
|
"notConfiguredUser": "تنظیمات اتصال در حال انجام است. لطفاً بعداً مراجعه کنید.",
|
||||||
|
"notConfiguredAdmin": "هیچ برنامه اتصالی پیکربندی نشده است. آنها را در تنظیمات برنامهها تنظیم کنید.",
|
||||||
|
"goToApps": "رفتن به تنظیمات برنامهها"
|
||||||
},
|
},
|
||||||
"additionalOptions": {
|
"additionalOptions": {
|
||||||
"title": "گزینههای اضافی",
|
"title": "گزینههای اضافی",
|
||||||
@@ -1072,6 +1076,7 @@
|
|||||||
"confirmDeleteText": "این عمل قابل بازگشت نیست. تعرفه برای همیشه حذف میشود.",
|
"confirmDeleteText": "این عمل قابل بازگشت نیست. تعرفه برای همیشه حذف میشود.",
|
||||||
"confirmDeleteWithSubscriptions": "این تعرفه {{count}} اشتراک فعال دارد. پس از حذف، کاربران هنگام تمدید باید تعرفه جدید انتخاب کنند.",
|
"confirmDeleteWithSubscriptions": "این تعرفه {{count}} اشتراک فعال دارد. پس از حذف، کاربران هنگام تمدید باید تعرفه جدید انتخاب کنند.",
|
||||||
"deleteSuccess": "تعرفه با موفقیت حذف شد",
|
"deleteSuccess": "تعرفه با موفقیت حذف شد",
|
||||||
|
"deleteSuccessWithSubscriptions": "تعرفه حذف شد. تعرفه {{count}} اشتراک بازنشانی شد — کاربران هنگام تمدید تعرفه جدید انتخاب میکنند.",
|
||||||
"saveOrder": "ذخیره ترتیب",
|
"saveOrder": "ذخیره ترتیب",
|
||||||
"orderSaved": "ترتیب ذخیره شد",
|
"orderSaved": "ترتیب ذخیره شد",
|
||||||
"dragToReorder": "برای تغییر ترتیب بکشید",
|
"dragToReorder": "برای تغییر ترتیب بکشید",
|
||||||
|
|||||||
@@ -377,7 +377,11 @@
|
|||||||
"selectPlatform": "Выберите платформу",
|
"selectPlatform": "Выберите платформу",
|
||||||
"app": "приложение",
|
"app": "приложение",
|
||||||
"apps": "приложений",
|
"apps": "приложений",
|
||||||
"tutorial": "Инструкция"
|
"tutorial": "Инструкция",
|
||||||
|
"notConfigured": "Способы подключения ещё не настроены",
|
||||||
|
"notConfiguredUser": "Настройка подключения в процессе. Пожалуйста, загляните позже.",
|
||||||
|
"notConfiguredAdmin": "Приложения для подключения не настроены. Настройте их в разделе Приложения.",
|
||||||
|
"goToApps": "Перейти к настройке приложений"
|
||||||
},
|
},
|
||||||
"myDevices": "Мои устройства",
|
"myDevices": "Мои устройства",
|
||||||
"noDevices": "Нет подключенных устройств",
|
"noDevices": "Нет подключенных устройств",
|
||||||
@@ -1884,6 +1888,7 @@
|
|||||||
"confirmDeleteText": "Это действие нельзя отменить. Тариф будет удалён навсегда.",
|
"confirmDeleteText": "Это действие нельзя отменить. Тариф будет удалён навсегда.",
|
||||||
"confirmDeleteWithSubscriptions": "У этого тарифа {{count}} активных подписок. При удалении пользователям потребуется выбрать новый тариф при продлении.",
|
"confirmDeleteWithSubscriptions": "У этого тарифа {{count}} активных подписок. При удалении пользователям потребуется выбрать новый тариф при продлении.",
|
||||||
"deleteSuccess": "Тариф успешно удален",
|
"deleteSuccess": "Тариф успешно удален",
|
||||||
|
"deleteSuccessWithSubscriptions": "Тариф удален. У {{count}} подписок сброшен тариф — при продлении пользователи выберут новый.",
|
||||||
"saveOrder": "Сохранить порядок",
|
"saveOrder": "Сохранить порядок",
|
||||||
"orderSaved": "Порядок сохранён",
|
"orderSaved": "Порядок сохранён",
|
||||||
"dragToReorder": "Перетащите для изменения порядка",
|
"dragToReorder": "Перетащите для изменения порядка",
|
||||||
|
|||||||
@@ -307,7 +307,11 @@
|
|||||||
"selectPlatform": "选择平台",
|
"selectPlatform": "选择平台",
|
||||||
"app": "个应用",
|
"app": "个应用",
|
||||||
"apps": "个应用",
|
"apps": "个应用",
|
||||||
"tutorial": "教程"
|
"tutorial": "教程",
|
||||||
|
"notConfigured": "连接方式尚未配置",
|
||||||
|
"notConfiguredUser": "连接设置正在进行中,请稍后再来查看。",
|
||||||
|
"notConfiguredAdmin": "尚未配置连接应用。请在应用设置中进行配置。",
|
||||||
|
"goToApps": "前往应用设置"
|
||||||
},
|
},
|
||||||
"additionalOptions": {
|
"additionalOptions": {
|
||||||
"title": "附加选项",
|
"title": "附加选项",
|
||||||
@@ -1110,6 +1114,7 @@
|
|||||||
"confirmDeleteText": "此操作不可撤销。套餐将被永久删除。",
|
"confirmDeleteText": "此操作不可撤销。套餐将被永久删除。",
|
||||||
"confirmDeleteWithSubscriptions": "此套餐有 {{count}} 个活跃订阅。删除后,用户续订时需要选择新套餐。",
|
"confirmDeleteWithSubscriptions": "此套餐有 {{count}} 个活跃订阅。删除后,用户续订时需要选择新套餐。",
|
||||||
"deleteSuccess": "套餐删除成功",
|
"deleteSuccess": "套餐删除成功",
|
||||||
|
"deleteSuccessWithSubscriptions": "套餐已删除。{{count}} 个订阅已重置 — 用户续订时将选择新套餐。",
|
||||||
"saveOrder": "保存排序",
|
"saveOrder": "保存排序",
|
||||||
"orderSaved": "排序已保存",
|
"orderSaved": "排序已保存",
|
||||||
"dragToReorder": "拖动以重新排序",
|
"dragToReorder": "拖动以重新排序",
|
||||||
|
|||||||
@@ -282,9 +282,15 @@ export default function AdminTariffs() {
|
|||||||
// Mutations
|
// Mutations
|
||||||
const deleteMutation = useMutation({
|
const deleteMutation = useMutation({
|
||||||
mutationFn: tariffsApi.deleteTariff,
|
mutationFn: tariffsApi.deleteTariff,
|
||||||
onSuccess: () => {
|
onSuccess: (data) => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['admin-tariffs'] });
|
queryClient.invalidateQueries({ queryKey: ['admin-tariffs'] });
|
||||||
notify.success(t('admin.tariffs.deleteSuccess'));
|
if (data.affected_subscriptions > 0) {
|
||||||
|
notify.success(
|
||||||
|
t('admin.tariffs.deleteSuccessWithSubscriptions', { count: data.affected_subscriptions }),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
notify.success(t('admin.tariffs.deleteSuccess'));
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useCallback, useRef } from 'react';
|
import { useEffect, useCallback, useMemo, useRef } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { Link, useNavigate } from 'react-router';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { openLink as sdkOpenLink } from '@telegram-apps/sdk-react';
|
import { openLink as sdkOpenLink } from '@telegram-apps/sdk-react';
|
||||||
@@ -8,13 +8,13 @@ import { useTelegramSDK } from '../hooks/useTelegramSDK';
|
|||||||
import { useHaptic } from '@/platform';
|
import { useHaptic } from '@/platform';
|
||||||
import { resolveTemplate, hasTemplates } from '../utils/templateEngine';
|
import { resolveTemplate, hasTemplates } from '../utils/templateEngine';
|
||||||
import { useAuthStore } from '../store/auth';
|
import { useAuthStore } from '../store/auth';
|
||||||
import type { AppConfig } from '../types';
|
import type { AppConfig, RemnawavePlatformData } from '../types';
|
||||||
import InstallationGuide from '../components/connection/InstallationGuide';
|
import InstallationGuide from '../components/connection/InstallationGuide';
|
||||||
|
|
||||||
export default function Connection() {
|
export default function Connection() {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { user } = useAuthStore();
|
const { user, isAdmin } = useAuthStore();
|
||||||
const { isTelegramWebApp } = useTelegramSDK();
|
const { isTelegramWebApp } = useTelegramSDK();
|
||||||
const { impact: hapticImpact } = useHaptic();
|
const { impact: hapticImpact } = useHaptic();
|
||||||
|
|
||||||
@@ -79,6 +79,14 @@ export default function Connection() {
|
|||||||
[isTelegramWebApp, i18n.language, resolveUrl],
|
[isTelegramWebApp, i18n.language, resolveUrl],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check if any platform has configured apps
|
||||||
|
const hasApps = useMemo(() => {
|
||||||
|
if (!appConfig?.platforms) return false;
|
||||||
|
return Object.values(appConfig.platforms).some(
|
||||||
|
(p: RemnawavePlatformData) => p.apps && p.apps.length > 0,
|
||||||
|
);
|
||||||
|
}, [appConfig?.platforms]);
|
||||||
|
|
||||||
// Loading
|
// Loading
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -100,6 +108,60 @@ export default function Connection() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No apps configured — check before subscription since empty config also has no subscription
|
||||||
|
if (!hasApps) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-col items-center justify-center p-8 text-center">
|
||||||
|
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-dark-800">
|
||||||
|
<svg
|
||||||
|
className="h-8 w-8 text-dark-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="mb-2 text-xl font-bold text-dark-100">
|
||||||
|
{t('subscription.connection.notConfigured')}
|
||||||
|
</h3>
|
||||||
|
<p className="mb-6 max-w-sm text-dark-400">
|
||||||
|
{isAdmin
|
||||||
|
? t('subscription.connection.notConfiguredAdmin')
|
||||||
|
: t('subscription.connection.notConfiguredUser')}
|
||||||
|
</p>
|
||||||
|
{isAdmin && (
|
||||||
|
<Link to="/admin/apps" className="btn-primary inline-flex items-center gap-2 px-6 py-2.5">
|
||||||
|
<svg
|
||||||
|
className="h-4 w-4"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M10.343 3.94c.09-.542.56-.94 1.11-.94h1.093c.55 0 1.02.398 1.11.94l.149.894c.07.424.384.764.78.93.398.164.855.142 1.205-.108l.737-.527a1.125 1.125 0 011.45.12l.773.774c.39.389.44 1.002.12 1.45l-.527.737c-.25.35-.272.806-.107 1.204.165.397.505.71.93.78l.893.15c.543.09.94.56.94 1.109v1.094c0 .55-.397 1.02-.94 1.11l-.893.149c-.425.07-.765.383-.93.78-.165.398-.143.854.107 1.204l.527.738c.32.447.269 1.06-.12 1.45l-.774.773a1.125 1.125 0 01-1.449.12l-.738-.527c-.35-.25-.806-.272-1.203-.107-.397.165-.71.505-.781.929l-.149.894c-.09.542-.56.94-1.11.94h-1.094c-.55 0-1.019-.398-1.11-.94l-.148-.894c-.071-.424-.384-.764-.781-.93-.398-.164-.854-.142-1.204.108l-.738.527c-.447.32-1.06.269-1.45-.12l-.773-.774a1.125 1.125 0 01-.12-1.45l.527-.737c.25-.35.273-.806.108-1.204-.165-.397-.505-.71-.93-.78l-.894-.15c-.542-.09-.94-.56-.94-1.109v-1.094c0-.55.398-1.02.94-1.11l.894-.149c.424-.07.765-.383.93-.78.165-.398.143-.854-.108-1.204l-.526-.738a1.125 1.125 0 01.12-1.45l.773-.773a1.125 1.125 0 011.45-.12l.737.527c.35.25.807.272 1.204.107.397-.165.71-.505.78-.929l.15-.894z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{t('subscription.connection.goToApps')}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// No subscription
|
// No subscription
|
||||||
if (!appConfig.hasSubscription) {
|
if (!appConfig.hasSubscription) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -67,9 +67,11 @@ export function useCloseOnSuccessNotification(onClose: () => void) {
|
|||||||
const onCloseRef = useRef(onClose);
|
const onCloseRef = useRef(onClose);
|
||||||
onCloseRef.current = onClose;
|
onCloseRef.current = onClose;
|
||||||
|
|
||||||
|
// Remember the signal value at mount time so we only react to NEW signals
|
||||||
|
const mountedSignalRef = useRef(closeOthersSignal);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Skip the initial render (signal = 0)
|
if (closeOthersSignal !== mountedSignalRef.current) {
|
||||||
if (closeOthersSignal > 0) {
|
|
||||||
onCloseRef.current();
|
onCloseRef.current();
|
||||||
}
|
}
|
||||||
}, [closeOthersSignal]);
|
}, [closeOthersSignal]);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export default defineConfig({
|
|||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
manualChunks(id) {
|
manualChunks(id) {
|
||||||
|
if (id.includes('/src/locales/')) return 'locales';
|
||||||
if (!id.includes('node_modules')) return;
|
if (!id.includes('node_modules')) return;
|
||||||
if (
|
if (
|
||||||
id.includes('react-dom') ||
|
id.includes('react-dom') ||
|
||||||
@@ -44,13 +45,17 @@ export default defineConfig({
|
|||||||
)
|
)
|
||||||
return 'vendor-react';
|
return 'vendor-react';
|
||||||
if (id.includes('@tanstack/react-query')) return 'vendor-query';
|
if (id.includes('@tanstack/react-query')) return 'vendor-query';
|
||||||
|
if (id.includes('@tanstack/react-table')) return 'vendor-table';
|
||||||
if (id.includes('i18next') || id.includes('react-i18next')) return 'vendor-i18n';
|
if (id.includes('i18next') || id.includes('react-i18next')) return 'vendor-i18n';
|
||||||
if (id.includes('framer-motion')) return 'vendor-motion';
|
if (id.includes('framer-motion')) return 'vendor-motion';
|
||||||
if (id.includes('@radix-ui/')) return 'vendor-radix';
|
if (id.includes('@radix-ui/')) return 'vendor-radix';
|
||||||
if (id.includes('@dnd-kit/')) return 'vendor-dnd';
|
if (id.includes('@dnd-kit/')) return 'vendor-dnd';
|
||||||
if (id.includes('@telegram-apps/')) return 'vendor-telegram';
|
if (id.includes('@telegram-apps/') || id.includes('/@tma.js/')) return 'vendor-telegram';
|
||||||
if (id.includes('/ogl/')) return 'vendor-webgl';
|
if (id.includes('/ogl/')) return 'vendor-webgl';
|
||||||
if (id.includes('/cmdk/')) return 'vendor-cmdk';
|
if (id.includes('/cmdk/')) return 'vendor-cmdk';
|
||||||
|
if (id.includes('twemoji') || id.includes('@twemoji/')) return 'vendor-twemoji';
|
||||||
|
if (id.includes('/jsencrypt/') || id.includes('@kastov/')) return 'vendor-crypto';
|
||||||
|
if (id.includes('@lottiefiles/')) return 'vendor-lottie';
|
||||||
if (
|
if (
|
||||||
id.includes('/axios/') ||
|
id.includes('/axios/') ||
|
||||||
id.includes('/zustand/') ||
|
id.includes('/zustand/') ||
|
||||||
|
|||||||
Reference in New Issue
Block a user