mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
Complete back button logic for all admin pages in Telegram Mini App
Main sections navigate to /admin (15 pages) Detail/edit pages navigate to parent lists (16 pages) All pages use useBackButton hook with proper navigation paths Native Telegram back button in Mini App, UI button on web Email template pages keep UI-only back buttons as required
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useRef } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
@@ -12,10 +13,23 @@ import {
|
|||||||
RemnawaveConfig,
|
RemnawaveConfig,
|
||||||
importFromRemnawaveFormat as convertRemnawave,
|
importFromRemnawaveFormat as convertRemnawave,
|
||||||
} from '../api/adminApps';
|
} from '../api/adminApps';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
|
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-dark-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
const AppsIcon = () => (
|
const AppsIcon = () => (
|
||||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
<path
|
<path
|
||||||
@@ -571,7 +585,13 @@ function AppEditorModal({ app, platform, isNew, onSave, onClose }: AppEditorModa
|
|||||||
|
|
||||||
export default function AdminApps() {
|
export default function AdminApps() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { capabilities } = usePlatform();
|
||||||
|
|
||||||
|
// Use native Telegram back button in Mini App
|
||||||
|
useBackButton(() => navigate('/admin'));
|
||||||
|
|
||||||
const [selectedPlatform, setSelectedPlatform] = useState<string>('ios');
|
const [selectedPlatform, setSelectedPlatform] = useState<string>('ios');
|
||||||
const [editingApp, setEditingApp] = useState<{ app: AppDefinition; isNew: boolean } | null>(null);
|
const [editingApp, setEditingApp] = useState<{ app: AppDefinition; isNew: boolean } | null>(null);
|
||||||
const [copyTarget, setCopyTarget] = useState<{ appId: string; platform: string } | null>(null);
|
const [copyTarget, setCopyTarget] = useState<{ appId: string; platform: string } | null>(null);
|
||||||
@@ -805,7 +825,15 @@ export default function AdminApps() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<AdminBackButton />
|
{/* Show back button only on web, not in Telegram Mini App */}
|
||||||
|
{!capabilities.hasBackButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/admin')}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<h1 className="text-2xl font-bold text-dark-50 sm:text-3xl">{t('admin.apps.title')}</h1>
|
<h1 className="text-2xl font-bold text-dark-50 sm:text-3xl">{t('admin.apps.title')}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
CombinedBroadcastCreateRequest,
|
CombinedBroadcastCreateRequest,
|
||||||
} from '../api/adminBroadcasts';
|
} from '../api/adminBroadcasts';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { AdminBackButton } from '../components/admin';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
const BroadcastIcon = () => (
|
const BroadcastIcon = () => (
|
||||||
@@ -118,6 +119,8 @@ export default function AdminBroadcastCreate() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useBackButton(() => navigate('/admin/broadcasts'));
|
||||||
|
|
||||||
// Channel selection
|
// Channel selection
|
||||||
const [channel, setChannel] = useState<BroadcastChannel>('telegram');
|
const [channel, setChannel] = useState<BroadcastChannel>('telegram');
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { adminBroadcastsApi, type BroadcastChannel } from '../api/adminBroadcasts';
|
import { adminBroadcastsApi, type BroadcastChannel } from '../api/adminBroadcasts';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { AdminBackButton } from '../components/admin';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
|
|
||||||
@@ -158,6 +159,8 @@ export default function AdminBroadcastDetail() {
|
|||||||
|
|
||||||
const broadcastId = id ? parseInt(id, 10) : null;
|
const broadcastId = id ? parseInt(id, 10) : null;
|
||||||
|
|
||||||
|
useBackButton(() => navigate('/admin/broadcasts'));
|
||||||
|
|
||||||
// Fetch broadcast details
|
// Fetch broadcast details
|
||||||
const {
|
const {
|
||||||
data: broadcast,
|
data: broadcast,
|
||||||
|
|||||||
@@ -3,10 +3,23 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { adminBroadcastsApi } from '../api/adminBroadcasts';
|
import { adminBroadcastsApi } from '../api/adminBroadcasts';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
|
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-dark-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
const BroadcastIcon = () => (
|
const BroadcastIcon = () => (
|
||||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
<path
|
<path
|
||||||
@@ -116,6 +129,10 @@ function StatusBadge({ status }: { status: string }) {
|
|||||||
export default function AdminBroadcasts() {
|
export default function AdminBroadcasts() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { capabilities } = usePlatform();
|
||||||
|
|
||||||
|
// Use native Telegram back button in Mini App
|
||||||
|
useBackButton(() => navigate('/admin'));
|
||||||
|
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
const limit = 20;
|
const limit = 20;
|
||||||
@@ -136,7 +153,15 @@ export default function AdminBroadcasts() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<AdminBackButton />
|
{/* Show back button only on web, not in Telegram Mini App */}
|
||||||
|
{!capabilities.hasBackButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/admin')}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-dark-100">{t('admin.broadcasts.title')}</h1>
|
<h1 className="text-xl font-bold text-dark-100">{t('admin.broadcasts.title')}</h1>
|
||||||
<p className="text-sm text-dark-400">{t('admin.broadcasts.subtitle')}</p>
|
<p className="text-sm text-dark-400">{t('admin.broadcasts.subtitle')}</p>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from '../api/campaigns';
|
} from '../api/campaigns';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { AdminBackButton } from '../components/admin';
|
||||||
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
|
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
const CampaignIcon = () => (
|
const CampaignIcon = () => (
|
||||||
@@ -155,6 +156,8 @@ export default function AdminCampaignCreate() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
useBackButton(() => navigate('/admin/campaigns'));
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [startParameter, setStartParameter] = useState('');
|
const [startParameter, setStartParameter] = useState('');
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { AdminBackButton } from '../components/admin';
|
import { AdminBackButton } from '../components/admin';
|
||||||
import { CheckIcon, CampaignIcon } from '../components/icons';
|
import { CheckIcon, CampaignIcon } from '../components/icons';
|
||||||
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
|
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
|
||||||
// Bonus type config
|
// Bonus type config
|
||||||
const bonusTypeConfig: Record<
|
const bonusTypeConfig: Record<
|
||||||
@@ -132,6 +133,8 @@ export default function AdminCampaignEdit() {
|
|||||||
|
|
||||||
const campaignId = parseInt(id || '0');
|
const campaignId = parseInt(id || '0');
|
||||||
|
|
||||||
|
useBackButton(() => navigate('/admin/campaigns'));
|
||||||
|
|
||||||
// Fetch campaign
|
// Fetch campaign
|
||||||
const {
|
const {
|
||||||
data: campaign,
|
data: campaign,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { campaignsApi, CampaignBonusType } from '../api/campaigns';
|
import { campaignsApi, CampaignBonusType } from '../api/campaigns';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { AdminBackButton } from '../components/admin';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
const CopyIcon = () => (
|
const CopyIcon = () => (
|
||||||
@@ -97,6 +98,8 @@ export default function AdminCampaignStats() {
|
|||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [showUsers, setShowUsers] = useState(false);
|
const [showUsers, setShowUsers] = useState(false);
|
||||||
|
|
||||||
|
useBackButton(() => navigate('/admin/campaigns'));
|
||||||
|
|
||||||
// Fetch stats
|
// Fetch stats
|
||||||
const {
|
const {
|
||||||
data: stats,
|
data: stats,
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import i18n from '../i18n';
|
import i18n from '../i18n';
|
||||||
import { campaignsApi, CampaignListItem, CampaignBonusType } from '../api/campaigns';
|
import { campaignsApi, CampaignListItem, CampaignBonusType } from '../api/campaigns';
|
||||||
import { AdminBackButton } from '../components/admin';
|
|
||||||
import { PlusIcon, EditIcon, TrashIcon, CheckIcon, XIcon, ChartIcon } from '../components/icons';
|
import { PlusIcon, EditIcon, TrashIcon, CheckIcon, XIcon, ChartIcon } from '../components/icons';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
|
|
||||||
// Bonus type labels and colors
|
// Bonus type labels and colors
|
||||||
const bonusTypeConfig: Record<
|
const bonusTypeConfig: Record<
|
||||||
@@ -34,6 +35,19 @@ const bonusTypeConfig: Record<
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-dark-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
// Locale mapping for formatting
|
// Locale mapping for formatting
|
||||||
const localeMap: Record<string, string> = { ru: 'ru-RU', en: 'en-US', zh: 'zh-CN', fa: 'fa-IR' };
|
const localeMap: Record<string, string> = { ru: 'ru-RU', en: 'en-US', zh: 'zh-CN', fa: 'fa-IR' };
|
||||||
|
|
||||||
@@ -51,6 +65,10 @@ export default function AdminCampaigns() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { capabilities } = usePlatform();
|
||||||
|
|
||||||
|
// Use native Telegram back button in Mini App
|
||||||
|
useBackButton(() => navigate('/admin'));
|
||||||
|
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState<number | null>(null);
|
const [deleteConfirm, setDeleteConfirm] = useState<number | null>(null);
|
||||||
|
|
||||||
@@ -89,7 +107,15 @@ export default function AdminCampaigns() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<AdminBackButton />
|
{/* Show back button only on web, not in Telegram Mini App */}
|
||||||
|
{!capabilities.hasBackButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/admin')}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-semibold text-dark-100">{t('admin.campaigns.title')}</h1>
|
<h1 className="text-xl font-semibold text-dark-100">{t('admin.campaigns.title')}</h1>
|
||||||
<p className="text-sm text-dark-400">{t('admin.campaigns.subtitle')}</p>
|
<p className="text-sm text-dark-400">{t('admin.campaigns.subtitle')}</p>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
statsApi,
|
statsApi,
|
||||||
@@ -9,10 +10,23 @@ import {
|
|||||||
type RecentPaymentsResponse,
|
type RecentPaymentsResponse,
|
||||||
} from '../api/admin';
|
} from '../api/admin';
|
||||||
import { useCurrency } from '../hooks/useCurrency';
|
import { useCurrency } from '../hooks/useCurrency';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
|
|
||||||
// Icons - styled like main navigation
|
// Icons - styled like main navigation
|
||||||
|
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-dark-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
const ServerIcon = () => (
|
const ServerIcon = () => (
|
||||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
<path
|
<path
|
||||||
@@ -347,7 +361,13 @@ function RevenueChart({ data }: { data: { date: string; amount_rubles: number }[
|
|||||||
|
|
||||||
export default function AdminDashboard() {
|
export default function AdminDashboard() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const { formatAmount, currencySymbol } = useCurrency();
|
const { formatAmount, currencySymbol } = useCurrency();
|
||||||
|
const { capabilities } = usePlatform();
|
||||||
|
|
||||||
|
// Use native Telegram back button in Mini App
|
||||||
|
useBackButton(() => navigate('/admin'));
|
||||||
|
|
||||||
const [stats, setStats] = useState<DashboardStats | null>(null);
|
const [stats, setStats] = useState<DashboardStats | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -450,7 +470,15 @@ export default function AdminDashboard() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<AdminBackButton />
|
{/* Show back button only on web, not in Telegram Mini App */}
|
||||||
|
{!capabilities.hasBackButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/admin')}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-dark-100">{t('adminDashboard.title')}</h1>
|
<h1 className="text-2xl font-bold text-dark-100">{t('adminDashboard.title')}</h1>
|
||||||
<p className="text-dark-400">{t('adminDashboard.subtitle')}</p>
|
<p className="text-dark-400">{t('adminDashboard.subtitle')}</p>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { adminPaymentMethodsApi } from '../api/adminPaymentMethods';
|
import { adminPaymentMethodsApi } from '../api/adminPaymentMethods';
|
||||||
import type { PromoGroupSimple } from '../types';
|
import type { PromoGroupSimple } from '../types';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
|
|
||||||
const BackIcon = () => (
|
const BackIcon = () => (
|
||||||
<svg
|
<svg
|
||||||
@@ -68,6 +70,10 @@ export default function AdminPaymentMethodEdit() {
|
|||||||
const { methodId } = useParams<{ methodId: string }>();
|
const { methodId } = useParams<{ methodId: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { capabilities } = usePlatform();
|
||||||
|
|
||||||
|
// Use native Telegram back button in Mini App
|
||||||
|
useBackButton(() => navigate('/admin/payment-methods'));
|
||||||
|
|
||||||
// Fetch payment methods
|
// Fetch payment methods
|
||||||
const { data: methods, isLoading } = useQuery({
|
const { data: methods, isLoading } = useQuery({
|
||||||
@@ -174,12 +180,15 @@ export default function AdminPaymentMethodEdit() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link
|
{/* Show back button only on web, not in Telegram Mini App */}
|
||||||
to="/admin/payment-methods"
|
{!capabilities.hasBackButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/admin/payment-methods')}
|
||||||
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
||||||
>
|
>
|
||||||
<BackIcon />
|
<BackIcon />
|
||||||
</Link>
|
</button>
|
||||||
|
)}
|
||||||
<h1 className="text-2xl font-bold text-dark-50">
|
<h1 className="text-2xl font-bold text-dark-50">
|
||||||
{t('admin.paymentMethods.notFound', 'Payment method not found')}
|
{t('admin.paymentMethods.notFound', 'Payment method not found')}
|
||||||
</h1>
|
</h1>
|
||||||
@@ -195,12 +204,15 @@ export default function AdminPaymentMethodEdit() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link
|
{/* Show back button only on web, not in Telegram Mini App */}
|
||||||
to="/admin/payment-methods"
|
{!capabilities.hasBackButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/admin/payment-methods')}
|
||||||
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
||||||
>
|
>
|
||||||
<BackIcon />
|
<BackIcon />
|
||||||
</Link>
|
</button>
|
||||||
|
)}
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-dark-700/50 text-xl">
|
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-dark-700/50 text-xl">
|
||||||
{icon}
|
{icon}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,35 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { adminPaymentsApi } from '../api/adminPayments';
|
import { adminPaymentsApi } from '../api/adminPayments';
|
||||||
import { useCurrency } from '../hooks/useCurrency';
|
import { useCurrency } from '../hooks/useCurrency';
|
||||||
import { AdminBackButton } from '../components/admin';
|
|
||||||
import type { PendingPayment, PaginatedResponse } from '../types';
|
import type { PendingPayment, PaginatedResponse } from '../types';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
|
|
||||||
|
// BackIcon
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-dark-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
export default function AdminPayments() {
|
export default function AdminPayments() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { formatAmount, currencySymbol } = useCurrency();
|
const { formatAmount, currencySymbol } = useCurrency();
|
||||||
|
const { capabilities } = usePlatform();
|
||||||
|
|
||||||
|
// Use native Telegram back button in Mini App
|
||||||
|
useBackButton(() => navigate('/admin'));
|
||||||
|
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [methodFilter, setMethodFilter] = useState<string>('');
|
const [methodFilter, setMethodFilter] = useState<string>('');
|
||||||
@@ -68,7 +88,15 @@ export default function AdminPayments() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<AdminBackButton />
|
{/* Show back button only on web, not in Telegram Mini App */}
|
||||||
|
{!capabilities.hasBackButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/admin')}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-dark-50">{t('admin.payments.title')}</h1>
|
<h1 className="text-2xl font-bold text-dark-50">{t('admin.payments.title')}</h1>
|
||||||
<p className="text-sm text-dark-400">{t('admin.payments.description')}</p>
|
<p className="text-sm text-dark-400">{t('admin.payments.description')}</p>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
PromoGroupUpdateRequest,
|
PromoGroupUpdateRequest,
|
||||||
} from '../api/promocodes';
|
} from '../api/promocodes';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { AdminBackButton } from '../components/admin';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
const PlusIcon = () => (
|
const PlusIcon = () => (
|
||||||
@@ -55,6 +56,8 @@ export default function AdminPromoGroupCreate() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const isEdit = !!id;
|
const isEdit = !!id;
|
||||||
|
|
||||||
|
useBackButton(() => navigate('/admin/promo-groups'));
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [serverDiscount, setServerDiscount] = useState<number | ''>(0);
|
const [serverDiscount, setServerDiscount] = useState<number | ''>(0);
|
||||||
|
|||||||
@@ -3,9 +3,22 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { promocodesApi, PromoGroup } from '../api/promocodes';
|
import { promocodesApi, PromoGroup } from '../api/promocodes';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-dark-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
const PlusIcon = () => (
|
const PlusIcon = () => (
|
||||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||||
@@ -46,6 +59,10 @@ export default function AdminPromoGroups() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { capabilities } = usePlatform();
|
||||||
|
|
||||||
|
// Use native Telegram back button in Mini App
|
||||||
|
useBackButton(() => navigate('/admin'));
|
||||||
|
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState<number | null>(null);
|
const [deleteConfirm, setDeleteConfirm] = useState<number | null>(null);
|
||||||
|
|
||||||
@@ -71,7 +88,15 @@ export default function AdminPromoGroups() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<AdminBackButton />
|
{/* Show back button only on web, not in Telegram Mini App */}
|
||||||
|
{!capabilities.hasBackButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/admin')}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-semibold text-dark-100">{t('admin.promoGroups.title')}</h1>
|
<h1 className="text-xl font-semibold text-dark-100">{t('admin.promoGroups.title')}</h1>
|
||||||
<p className="text-sm text-dark-400">{t('admin.promoGroups.subtitle')}</p>
|
<p className="text-sm text-dark-400">{t('admin.promoGroups.subtitle')}</p>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from '../api/promoOffers';
|
} from '../api/promoOffers';
|
||||||
import { adminUsersApi, UserListItem } from '../api/adminUsers';
|
import { adminUsersApi, UserListItem } from '../api/adminUsers';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { AdminBackButton } from '../components/admin';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
const SendIcon = () => (
|
const SendIcon = () => (
|
||||||
@@ -75,6 +76,8 @@ export default function AdminPromoOfferSend() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
useBackButton(() => navigate('/admin/promo-offers'));
|
||||||
|
|
||||||
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(null);
|
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(null);
|
||||||
const [sendMode, setSendMode] = useState<'segment' | 'user'>('segment');
|
const [sendMode, setSendMode] = useState<'segment' | 'user'>('segment');
|
||||||
const [selectedTarget, setSelectedTarget] = useState<TargetSegment>('active');
|
const [selectedTarget, setSelectedTarget] = useState<TargetSegment>('active');
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from '../api/promoOffers';
|
} from '../api/promoOffers';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { AdminBackButton } from '../components/admin';
|
||||||
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
|
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
|
||||||
const getOfferTypeIcon = (offerType: string): string => {
|
const getOfferTypeIcon = (offerType: string): string => {
|
||||||
return OFFER_TYPE_CONFIG[offerType as OfferType]?.icon || '🎁';
|
return OFFER_TYPE_CONFIG[offerType as OfferType]?.icon || '🎁';
|
||||||
@@ -21,6 +22,8 @@ export default function AdminPromoOfferTemplateEdit() {
|
|||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
useBackButton(() => navigate('/admin/promo-offers'));
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [messageText, setMessageText] = useState('');
|
const [messageText, setMessageText] = useState('');
|
||||||
|
|||||||
@@ -4,9 +4,22 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import i18n from '../i18n';
|
import i18n from '../i18n';
|
||||||
import { promoOffersApi, PromoOfferLog, OFFER_TYPE_CONFIG, OfferType } from '../api/promoOffers';
|
import { promoOffersApi, PromoOfferLog, OFFER_TYPE_CONFIG, OfferType } from '../api/promoOffers';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-dark-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
const EditIcon = () => (
|
const EditIcon = () => (
|
||||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
<path
|
<path
|
||||||
@@ -93,6 +106,10 @@ const getOfferTypeLabel = (offerType: string): string => {
|
|||||||
export default function AdminPromoOffers() {
|
export default function AdminPromoOffers() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { capabilities } = usePlatform();
|
||||||
|
|
||||||
|
// Use native Telegram back button in Mini App
|
||||||
|
useBackButton(() => navigate('/admin'));
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<'templates' | 'logs'>('templates');
|
const [activeTab, setActiveTab] = useState<'templates' | 'logs'>('templates');
|
||||||
|
|
||||||
@@ -116,7 +133,15 @@ export default function AdminPromoOffers() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<AdminBackButton />
|
{/* Show back button only on web, not in Telegram Mini App */}
|
||||||
|
{!capabilities.hasBackButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/admin')}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-semibold text-dark-100">{t('admin.promoOffers.title')}</h1>
|
<h1 className="text-xl font-semibold text-dark-100">{t('admin.promoOffers.title')}</h1>
|
||||||
<p className="text-sm text-dark-400">{t('admin.promoOffers.subtitle')}</p>
|
<p className="text-sm text-dark-400">{t('admin.promoOffers.subtitle')}</p>
|
||||||
|
|||||||
@@ -10,9 +10,22 @@ import {
|
|||||||
PromoCodeUpdateRequest,
|
PromoCodeUpdateRequest,
|
||||||
PromoGroup,
|
PromoGroup,
|
||||||
} from '../api/promocodes';
|
} from '../api/promocodes';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-dark-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
const RefreshIcon = () => (
|
const RefreshIcon = () => (
|
||||||
<svg
|
<svg
|
||||||
className="h-4 w-4 animate-spin"
|
className="h-4 w-4 animate-spin"
|
||||||
@@ -34,8 +47,12 @@ export default function AdminPromocodeCreate() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { capabilities } = usePlatform();
|
||||||
const isEdit = !!id;
|
const isEdit = !!id;
|
||||||
|
|
||||||
|
// Use native Telegram back button in Mini App
|
||||||
|
useBackButton(() => navigate('/admin/promocodes'));
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const [code, setCode] = useState('');
|
const [code, setCode] = useState('');
|
||||||
const [type, setType] = useState<PromoCodeType>('balance');
|
const [type, setType] = useState<PromoCodeType>('balance');
|
||||||
@@ -186,7 +203,15 @@ export default function AdminPromocodeCreate() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<AdminBackButton to="/admin/promocodes" />
|
{/* Show back button only on web, not in Telegram Mini App */}
|
||||||
|
{!capabilities.hasBackButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/admin/promocodes')}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-dark-100">
|
<h1 className="text-xl font-bold text-dark-100">
|
||||||
{isEdit
|
{isEdit
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import i18n from '../i18n';
|
import i18n from '../i18n';
|
||||||
import { promocodesApi, PromoCodeType } from '../api/promocodes';
|
import { promocodesApi, PromoCodeType } from '../api/promocodes';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { AdminBackButton } from '../components/admin';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
const EditIcon = () => (
|
const EditIcon = () => (
|
||||||
@@ -88,6 +89,8 @@ export default function AdminPromocodeStats() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
|
||||||
|
useBackButton(() => navigate('/admin/promocodes'));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: promocode,
|
data: promocode,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
|||||||
@@ -4,10 +4,23 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import i18n from '../i18n';
|
import i18n from '../i18n';
|
||||||
import { promocodesApi, PromoCode, PromoCodeType } from '../api/promocodes';
|
import { promocodesApi, PromoCode, PromoCodeType } from '../api/promocodes';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
|
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-dark-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
const PlusIcon = () => (
|
const PlusIcon = () => (
|
||||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||||
@@ -98,6 +111,10 @@ export default function AdminPromocodes() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { capabilities } = usePlatform();
|
||||||
|
|
||||||
|
// Use native Telegram back button in Mini App
|
||||||
|
useBackButton(() => navigate('/admin'));
|
||||||
|
|
||||||
const [deleteConfirm, setDeleteConfirm] = useState<number | null>(null);
|
const [deleteConfirm, setDeleteConfirm] = useState<number | null>(null);
|
||||||
const [copiedCode, setCopiedCode] = useState<string | null>(null);
|
const [copiedCode, setCopiedCode] = useState<string | null>(null);
|
||||||
@@ -130,7 +147,15 @@ export default function AdminPromocodes() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<AdminBackButton />
|
{/* Show back button only on web, not in Telegram Mini App */}
|
||||||
|
{!capabilities.hasBackButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/admin')}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-semibold text-dark-100">{t('admin.promocodes.title')}</h1>
|
<h1 className="text-xl font-semibold text-dark-100">{t('admin.promocodes.title')}</h1>
|
||||||
<p className="text-sm text-dark-400">{t('admin.promocodes.subtitle')}</p>
|
<p className="text-sm text-dark-400">{t('admin.promocodes.subtitle')}</p>
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import {
|
|||||||
SystemStatsResponse,
|
SystemStatsResponse,
|
||||||
AutoSyncStatus,
|
AutoSyncStatus,
|
||||||
} from '../api/adminRemnawave';
|
} from '../api/adminRemnawave';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
import {
|
import {
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
ChartIcon,
|
ChartIcon,
|
||||||
@@ -23,6 +24,20 @@ import {
|
|||||||
RemnawaveIcon,
|
RemnawaveIcon,
|
||||||
} from '../components/icons';
|
} from '../components/icons';
|
||||||
|
|
||||||
|
// ============ Icons ============
|
||||||
|
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-dark-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
// ============ Helpers ============
|
// ============ Helpers ============
|
||||||
|
|
||||||
const formatBytes = (bytes: number): string => {
|
const formatBytes = (bytes: number): string => {
|
||||||
@@ -861,6 +876,10 @@ export default function AdminRemnawave() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { capabilities } = usePlatform();
|
||||||
|
|
||||||
|
// Use native Telegram back button in Mini App
|
||||||
|
useBackButton(() => navigate('/admin'));
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const [activeTab, setActiveTab] = useState<TabType>('overview');
|
const [activeTab, setActiveTab] = useState<TabType>('overview');
|
||||||
@@ -996,7 +1015,15 @@ export default function AdminRemnawave() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<div className="mb-6 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<AdminBackButton />
|
{/* Show back button only on web, not in Telegram Mini App */}
|
||||||
|
{!capabilities.hasBackButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/admin')}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div className="rounded-lg bg-accent-500/20 p-2">
|
<div className="rounded-lg bg-accent-500/20 p-2">
|
||||||
<RemnawaveIcon className="h-6 w-6 text-accent-400" />
|
<RemnawaveIcon className="h-6 w-6 text-accent-400" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { adminRemnawaveApi, SquadWithLocalInfo } from '../api/adminRemnawave';
|
import { adminRemnawaveApi, SquadWithLocalInfo } from '../api/adminRemnawave';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { AdminBackButton } from '../components/admin';
|
||||||
import { ServerIcon, UsersIcon, CheckIcon, XIcon } from '../components/icons';
|
import { ServerIcon, UsersIcon, CheckIcon, XIcon } from '../components/icons';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
|
||||||
// Country flags helper
|
// Country flags helper
|
||||||
const getCountryFlag = (code: string | null | undefined): string => {
|
const getCountryFlag = (code: string | null | undefined): string => {
|
||||||
@@ -54,6 +55,8 @@ export default function AdminRemnawaveSquadDetail() {
|
|||||||
const { uuid } = useParams<{ uuid: string }>();
|
const { uuid } = useParams<{ uuid: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useBackButton(() => navigate('/admin/remnawave'));
|
||||||
|
|
||||||
// Fetch all squads and find the one we need
|
// Fetch all squads and find the one we need
|
||||||
const {
|
const {
|
||||||
data: squadsData,
|
data: squadsData,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { serversApi, ServerUpdateRequest } from '../api/servers';
|
|||||||
import { AdminBackButton } from '../components/admin';
|
import { AdminBackButton } from '../components/admin';
|
||||||
import { ServerIcon } from '../components/icons';
|
import { ServerIcon } from '../components/icons';
|
||||||
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
|
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
|
||||||
// Country flags (simple emoji mapping)
|
// Country flags (simple emoji mapping)
|
||||||
const getCountryFlag = (code: string | null): string => {
|
const getCountryFlag = (code: string | null): string => {
|
||||||
@@ -48,6 +49,8 @@ export default function AdminServerEdit() {
|
|||||||
|
|
||||||
const serverId = parseInt(id || '0');
|
const serverId = parseInt(id || '0');
|
||||||
|
|
||||||
|
useBackButton(() => navigate('/admin/servers'));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: server,
|
data: server,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
|||||||
@@ -2,8 +2,22 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { serversApi, ServerListItem } from '../api/servers';
|
import { serversApi, ServerListItem } from '../api/servers';
|
||||||
import { AdminBackButton } from '../components/admin';
|
|
||||||
import { SyncIcon, EditIcon, CheckIcon, XIcon, UsersIcon, GiftIcon } from '../components/icons';
|
import { SyncIcon, EditIcon, CheckIcon, XIcon, UsersIcon, GiftIcon } from '../components/icons';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
|
|
||||||
|
// BackIcon
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-dark-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
// Country flags (simple emoji mapping)
|
// Country flags (simple emoji mapping)
|
||||||
const getCountryFlag = (code: string | null): string => {
|
const getCountryFlag = (code: string | null): string => {
|
||||||
@@ -42,6 +56,10 @@ export default function AdminServers() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { capabilities } = usePlatform();
|
||||||
|
|
||||||
|
// Use native Telegram back button in Mini App
|
||||||
|
useBackButton(() => navigate('/admin'));
|
||||||
|
|
||||||
// Queries
|
// Queries
|
||||||
const { data: serversData, isLoading } = useQuery({
|
const { data: serversData, isLoading } = useQuery({
|
||||||
@@ -79,7 +97,15 @@ export default function AdminServers() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<AdminBackButton />
|
{/* Show back button only on web, not in Telegram Mini App */}
|
||||||
|
{!capabilities.hasBackButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/admin')}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-semibold text-dark-100">{t('admin.servers.title')}</h1>
|
<h1 className="text-xl font-semibold text-dark-100">{t('admin.servers.title')}</h1>
|
||||||
<p className="text-sm text-dark-400">{t('admin.servers.subtitle')}</p>
|
<p className="text-sm text-dark-400">{t('admin.servers.subtitle')}</p>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { useState, useMemo, useEffect, useCallback } from 'react';
|
import { useState, useMemo, useEffect, useCallback } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { adminSettingsApi, SettingDefinition } from '../api/adminSettings';
|
import { adminSettingsApi, SettingDefinition } from '../api/adminSettings';
|
||||||
import { themeColorsApi } from '../api/themeColors';
|
import { themeColorsApi } from '../api/themeColors';
|
||||||
import { useFavoriteSettings } from '../hooks/useFavoriteSettings';
|
import { useFavoriteSettings } from '../hooks/useFavoriteSettings';
|
||||||
import { MENU_SECTIONS, MenuItem, formatSettingKey, AdminBackButton } from '../components/admin';
|
import { MENU_SECTIONS, MenuItem, formatSettingKey } from '../components/admin';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
import { AnalyticsTab } from '../components/admin/AnalyticsTab';
|
import { AnalyticsTab } from '../components/admin/AnalyticsTab';
|
||||||
import { BrandingTab } from '../components/admin/BrandingTab';
|
import { BrandingTab } from '../components/admin/BrandingTab';
|
||||||
import { ThemeTab } from '../components/admin/ThemeTab';
|
import { ThemeTab } from '../components/admin/ThemeTab';
|
||||||
@@ -17,6 +20,19 @@ import {
|
|||||||
SettingsSearchResults,
|
SettingsSearchResults,
|
||||||
} from '../components/admin/SettingsSearch';
|
} from '../components/admin/SettingsSearch';
|
||||||
|
|
||||||
|
// BackIcon
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-dark-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
// Find section ID by category key
|
// Find section ID by category key
|
||||||
function findSectionByCategory(categoryKey: string): string | null {
|
function findSectionByCategory(categoryKey: string): string | null {
|
||||||
for (const section of MENU_SECTIONS) {
|
for (const section of MENU_SECTIONS) {
|
||||||
@@ -31,6 +47,11 @@ function findSectionByCategory(categoryKey: string): string | null {
|
|||||||
|
|
||||||
export default function AdminSettings() {
|
export default function AdminSettings() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { capabilities } = usePlatform();
|
||||||
|
|
||||||
|
// Use native Telegram back button in Mini App
|
||||||
|
useBackButton(() => navigate('/admin'));
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const [activeSection, setActiveSection] = useState('branding');
|
const [activeSection, setActiveSection] = useState('branding');
|
||||||
@@ -216,7 +237,15 @@ export default function AdminSettings() {
|
|||||||
<div className="w-64 shrink-0 overflow-y-auto border-r border-dark-700/50">
|
<div className="w-64 shrink-0 overflow-y-auto border-r border-dark-700/50">
|
||||||
<div className="border-b border-dark-700/50 p-4">
|
<div className="border-b border-dark-700/50 p-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<AdminBackButton />
|
{/* Show back button only on web, not in Telegram Mini App */}
|
||||||
|
{!capabilities.hasBackButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/admin')}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<h1 className="text-lg font-bold text-dark-100">{t('admin.settings.title')}</h1>
|
<h1 className="text-lg font-bold text-dark-100">{t('admin.settings.title')}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
} from '../api/tariffs';
|
} from '../api/tariffs';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { AdminBackButton } from '../components/admin';
|
||||||
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
|
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
const PlusIcon = () => (
|
const PlusIcon = () => (
|
||||||
@@ -91,6 +92,8 @@ export default function AdminTariffCreate() {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const isEdit = !!id;
|
const isEdit = !!id;
|
||||||
|
|
||||||
|
useBackButton(() => navigate('/admin/tariffs'));
|
||||||
|
|
||||||
// Step: null = type selection, 'period' or 'daily' = form
|
// Step: null = type selection, 'period' or 'daily' = form
|
||||||
const [tariffType, setTariffType] = useState<TariffType>(null);
|
const [tariffType, setTariffType] = useState<TariffType>(null);
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { tariffsApi, TariffListItem } from '../api/tariffs';
|
import { tariffsApi, TariffListItem } from '../api/tariffs';
|
||||||
import { AdminBackButton } from '../components/admin';
|
|
||||||
import { useDestructiveConfirm, useNotify } from '@/platform';
|
import { useDestructiveConfirm, useNotify } from '@/platform';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
const PlusIcon = () => (
|
const PlusIcon = () => (
|
||||||
@@ -54,12 +55,29 @@ const GiftIcon = () => (
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// BackIcon
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-dark-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
export default function AdminTariffs() {
|
export default function AdminTariffs() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const confirmDelete = useDestructiveConfirm();
|
const confirmDelete = useDestructiveConfirm();
|
||||||
const notify = useNotify();
|
const notify = useNotify();
|
||||||
|
const { capabilities } = usePlatform();
|
||||||
|
|
||||||
|
// Use native Telegram back button in Mini App
|
||||||
|
useBackButton(() => navigate('/admin'));
|
||||||
|
|
||||||
// Queries
|
// Queries
|
||||||
const { data: tariffsData, isLoading } = useQuery({
|
const { data: tariffsData, isLoading } = useQuery({
|
||||||
@@ -117,7 +135,15 @@ export default function AdminTariffs() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
<div className="mb-6 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<AdminBackButton />
|
{/* Show back button only on web, not in Telegram Mini App */}
|
||||||
|
{!capabilities.hasBackButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/admin')}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-semibold text-dark-100">{t('admin.tariffs.title')}</h1>
|
<h1 className="text-xl font-semibold text-dark-100">{t('admin.tariffs.title')}</h1>
|
||||||
<p className="text-sm text-dark-400">{t('admin.tariffs.subtitle')}</p>
|
<p className="text-sm text-dark-400">{t('admin.tariffs.subtitle')}</p>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { adminApi } from '../api/admin';
|
import { adminApi } from '../api/admin';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { AdminBackButton } from '../components/admin';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
|
||||||
const SettingsIcon = () => (
|
const SettingsIcon = () => (
|
||||||
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
@@ -21,6 +22,8 @@ export default function AdminTicketSettings() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
useBackButton(() => navigate('/admin/tickets'));
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: settings,
|
data: settings,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { adminApi, AdminTicket, AdminTicketDetail, AdminTicketMessage } from '../api/admin';
|
import { adminApi, AdminTicket, AdminTicketDetail, AdminTicketMessage } from '../api/admin';
|
||||||
import { ticketsApi } from '../api/tickets';
|
import { ticketsApi } from '../api/tickets';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
|
|
||||||
function AdminMessageMedia({
|
function AdminMessageMedia({
|
||||||
message,
|
message,
|
||||||
@@ -103,10 +104,28 @@ function AdminMessageMedia({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BackIcon
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-dark-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
export default function AdminTickets() {
|
export default function AdminTickets() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { capabilities } = usePlatform();
|
||||||
|
|
||||||
|
// Use native Telegram back button in Mini App
|
||||||
|
useBackButton(() => navigate('/admin'));
|
||||||
|
|
||||||
const [selectedTicketId, setSelectedTicketId] = useState<number | null>(null);
|
const [selectedTicketId, setSelectedTicketId] = useState<number | null>(null);
|
||||||
const [statusFilter, setStatusFilter] = useState<string>('');
|
const [statusFilter, setStatusFilter] = useState<string>('');
|
||||||
const [replyText, setReplyText] = useState('');
|
const [replyText, setReplyText] = useState('');
|
||||||
@@ -209,7 +228,15 @@ export default function AdminTickets() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<AdminBackButton />
|
{/* Show back button only on web, not in Telegram Mini App */}
|
||||||
|
{!capabilities.hasBackButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/admin')}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<h1 className="text-2xl font-bold text-dark-50 sm:text-3xl">
|
<h1 className="text-2xl font-bold text-dark-50 sm:text-3xl">
|
||||||
{t('admin.tickets.title')}
|
{t('admin.tickets.title')}
|
||||||
</h1>
|
</h1>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
type UpdateSubscriptionRequest,
|
type UpdateSubscriptionRequest,
|
||||||
} from '../api/adminUsers';
|
} from '../api/adminUsers';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { AdminBackButton } from '../components/admin';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
|
||||||
// ============ Icons ============
|
// ============ Icons ============
|
||||||
|
|
||||||
@@ -81,6 +82,8 @@ export default function AdminUserDetail() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
|
|
||||||
|
useBackButton(() => navigate('/admin/users'));
|
||||||
|
|
||||||
const localeMap: Record<string, string> = { ru: 'ru-RU', en: 'en-US', zh: 'zh-CN', fa: 'fa-IR' };
|
const localeMap: Record<string, string> = { ru: 'ru-RU', en: 'en-US', zh: 'zh-CN', fa: 'fa-IR' };
|
||||||
const locale = localeMap[i18n.language] || 'ru-RU';
|
const locale = localeMap[i18n.language] || 'ru-RU';
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,23 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useCurrency } from '../hooks/useCurrency';
|
import { useCurrency } from '../hooks/useCurrency';
|
||||||
import { useToast } from '../components/Toast';
|
import { useToast } from '../components/Toast';
|
||||||
import { adminUsersApi, type UserListItem, type UsersStatsResponse } from '../api/adminUsers';
|
import { adminUsersApi, type UserListItem, type UsersStatsResponse } from '../api/adminUsers';
|
||||||
import { AdminBackButton } from '../components/admin';
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
|
|
||||||
// ============ Icons ============
|
// ============ Icons ============
|
||||||
|
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-dark-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
const SearchIcon = () => (
|
const SearchIcon = () => (
|
||||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||||
<path
|
<path
|
||||||
@@ -383,6 +396,10 @@ export default function AdminUsers() {
|
|||||||
const { formatWithCurrency } = useCurrency();
|
const { formatWithCurrency } = useCurrency();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
|
const { capabilities } = usePlatform();
|
||||||
|
|
||||||
|
// Use native Telegram back button in Mini App
|
||||||
|
useBackButton(() => navigate('/admin'));
|
||||||
|
|
||||||
const [users, setUsers] = useState<UserListItem[]>([]);
|
const [users, setUsers] = useState<UserListItem[]>([]);
|
||||||
const [stats, setStats] = useState<UsersStatsResponse | null>(null);
|
const [stats, setStats] = useState<UsersStatsResponse | null>(null);
|
||||||
@@ -551,7 +568,15 @@ export default function AdminUsers() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="mb-6 flex items-center justify-between">
|
<div className="mb-6 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<AdminBackButton />
|
{/* Show back button only on web, not in Telegram Mini App */}
|
||||||
|
{!capabilities.hasBackButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/admin')}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-xl font-bold text-dark-100">{t('admin.users.title')}</h1>
|
<h1 className="text-xl font-bold text-dark-100">{t('admin.users.title')}</h1>
|
||||||
<p className="text-sm text-dark-400">{t('admin.users.subtitle')}</p>
|
<p className="text-sm text-dark-400">{t('admin.users.subtitle')}</p>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useCallback, useEffect } from 'react';
|
import { useState, useCallback, useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import {
|
import {
|
||||||
@@ -20,12 +21,25 @@ import {
|
|||||||
} from '@dnd-kit/sortable';
|
} from '@dnd-kit/sortable';
|
||||||
import { CSS } from '@dnd-kit/utilities';
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
import { adminWheelApi, type WheelPrizeAdmin, type CreateWheelPrizeData } from '../api/wheel';
|
import { adminWheelApi, type WheelPrizeAdmin, type CreateWheelPrizeData } from '../api/wheel';
|
||||||
import { AdminBackButton } from '../components/admin';
|
|
||||||
import { useDestructiveConfirm } from '@/platform';
|
import { useDestructiveConfirm } from '@/platform';
|
||||||
import FortuneWheel from '../components/wheel/FortuneWheel';
|
import FortuneWheel from '../components/wheel/FortuneWheel';
|
||||||
|
import { useBackButton } from '../platform/hooks/useBackButton';
|
||||||
|
import { usePlatform } from '../platform/hooks/usePlatform';
|
||||||
|
|
||||||
// Icons
|
// Icons
|
||||||
|
|
||||||
|
const BackIcon = () => (
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5 text-dark-400"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
const CogIcon = () => (
|
const CogIcon = () => (
|
||||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
<path
|
<path
|
||||||
@@ -272,8 +286,14 @@ function SortablePrizeCard({
|
|||||||
|
|
||||||
export default function AdminWheel() {
|
export default function AdminWheel() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const confirmDelete = useDestructiveConfirm();
|
const confirmDelete = useDestructiveConfirm();
|
||||||
|
const { capabilities } = usePlatform();
|
||||||
|
|
||||||
|
// Use native Telegram back button in Mini App
|
||||||
|
useBackButton(() => navigate('/admin'));
|
||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<Tab>('settings');
|
const [activeTab, setActiveTab] = useState<Tab>('settings');
|
||||||
const [expandedPrizeId, setExpandedPrizeId] = useState<number | null>(null);
|
const [expandedPrizeId, setExpandedPrizeId] = useState<number | null>(null);
|
||||||
const [isCreating, setIsCreating] = useState(false);
|
const [isCreating, setIsCreating] = useState(false);
|
||||||
@@ -487,7 +507,15 @@ export default function AdminWheel() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<AdminBackButton />
|
{/* Show back button only on web, not in Telegram Mini App */}
|
||||||
|
{!capabilities.hasBackButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/admin')}
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-xl border border-dark-700 bg-dark-800 transition-colors hover:border-dark-600"
|
||||||
|
>
|
||||||
|
<BackIcon />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<h1 className="text-xl font-bold text-dark-50 sm:text-2xl">{t('admin.wheel.title')}</h1>
|
<h1 className="text-xl font-bold text-dark-50 sm:text-2xl">{t('admin.wheel.title')}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user