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:
c0mrade
2026-02-04 05:03:06 +03:00
parent 98d90a99c6
commit e948cdc8d2
31 changed files with 522 additions and 45 deletions

View File

@@ -1,4 +1,5 @@
import { useState, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
@@ -12,10 +13,23 @@ import {
RemnawaveConfig,
importFromRemnawaveFormat as convertRemnawave,
} from '../api/adminApps';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
// 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 = () => (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
@@ -571,7 +585,13 @@ function AppEditorModal({ app, platform, isNew, onSave, onClose }: AppEditorModa
export default function AdminApps() {
const { t } = useTranslation();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
const [selectedPlatform, setSelectedPlatform] = useState<string>('ios');
const [editingApp, setEditingApp] = useState<{ app: AppDefinition; isNew: boolean } | 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="flex flex-wrap items-center justify-between 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>
</div>

View File

@@ -10,6 +10,7 @@ import {
CombinedBroadcastCreateRequest,
} from '../api/adminBroadcasts';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
// Icons
const BroadcastIcon = () => (
@@ -118,6 +119,8 @@ export default function AdminBroadcastCreate() {
const queryClient = useQueryClient();
const fileInputRef = useRef<HTMLInputElement>(null);
useBackButton(() => navigate('/admin/broadcasts'));
// Channel selection
const [channel, setChannel] = useState<BroadcastChannel>('telegram');

View File

@@ -3,6 +3,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { adminBroadcastsApi, type BroadcastChannel } from '../api/adminBroadcasts';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
// Icons
@@ -158,6 +159,8 @@ export default function AdminBroadcastDetail() {
const broadcastId = id ? parseInt(id, 10) : null;
useBackButton(() => navigate('/admin/broadcasts'));
// Fetch broadcast details
const {
data: broadcast,

View File

@@ -3,10 +3,23 @@ import { useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { adminBroadcastsApi } from '../api/adminBroadcasts';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
// 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 = () => (
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
@@ -116,6 +129,10 @@ function StatusBadge({ status }: { status: string }) {
export default function AdminBroadcasts() {
const { t } = useTranslation();
const navigate = useNavigate();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
const [page, setPage] = useState(0);
const limit = 20;
@@ -136,7 +153,15 @@ export default function AdminBroadcasts() {
{/* Header */}
<div className="flex items-center justify-between">
<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>
<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>

View File

@@ -11,6 +11,7 @@ import {
} from '../api/campaigns';
import { AdminBackButton } from '../components/admin';
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
import { useBackButton } from '../platform/hooks/useBackButton';
// Icons
const CampaignIcon = () => (
@@ -155,6 +156,8 @@ export default function AdminCampaignCreate() {
const navigate = useNavigate();
const queryClient = useQueryClient();
useBackButton(() => navigate('/admin/campaigns'));
// Form state
const [name, setName] = useState('');
const [startParameter, setStartParameter] = useState('');

View File

@@ -12,6 +12,7 @@ import {
import { AdminBackButton } from '../components/admin';
import { CheckIcon, CampaignIcon } from '../components/icons';
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
import { useBackButton } from '../platform/hooks/useBackButton';
// Bonus type config
const bonusTypeConfig: Record<
@@ -132,6 +133,8 @@ export default function AdminCampaignEdit() {
const campaignId = parseInt(id || '0');
useBackButton(() => navigate('/admin/campaigns'));
// Fetch campaign
const {
data: campaign,

View File

@@ -4,6 +4,7 @@ import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { campaignsApi, CampaignBonusType } from '../api/campaigns';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
// Icons
const CopyIcon = () => (
@@ -97,6 +98,8 @@ export default function AdminCampaignStats() {
const [copied, setCopied] = useState(false);
const [showUsers, setShowUsers] = useState(false);
useBackButton(() => navigate('/admin/campaigns'));
// Fetch stats
const {
data: stats,

View File

@@ -4,8 +4,9 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import i18n from '../i18n';
import { campaignsApi, CampaignListItem, CampaignBonusType } from '../api/campaigns';
import { AdminBackButton } from '../components/admin';
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
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
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 navigate = useNavigate();
const queryClient = useQueryClient();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
const [deleteConfirm, setDeleteConfirm] = useState<number | null>(null);
@@ -89,7 +107,15 @@ export default function AdminCampaigns() {
{/* Header */}
<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">
<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>
<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>

View File

@@ -1,4 +1,5 @@
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import {
statsApi,
@@ -9,10 +10,23 @@ import {
type RecentPaymentsResponse,
} from '../api/admin';
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
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 = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
@@ -347,7 +361,13 @@ function RevenueChart({ data }: { data: { date: string; amount_rubles: number }[
export default function AdminDashboard() {
const { t } = useTranslation();
const navigate = useNavigate();
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 [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -450,7 +470,15 @@ export default function AdminDashboard() {
{/* Header */}
<div className="flex items-center justify-between">
<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>
<h1 className="text-2xl font-bold text-dark-100">{t('adminDashboard.title')}</h1>
<p className="text-dark-400">{t('adminDashboard.subtitle')}</p>

View File

@@ -1,9 +1,11 @@
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 { useTranslation } from 'react-i18next';
import { adminPaymentMethodsApi } from '../api/adminPaymentMethods';
import type { PromoGroupSimple } from '../types';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
const BackIcon = () => (
<svg
@@ -68,6 +70,10 @@ export default function AdminPaymentMethodEdit() {
const { methodId } = useParams<{ methodId: string }>();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin/payment-methods'));
// Fetch payment methods
const { data: methods, isLoading } = useQuery({
@@ -174,12 +180,15 @@ export default function AdminPaymentMethodEdit() {
return (
<div className="space-y-6">
<div className="flex items-center gap-3">
<Link
to="/admin/payment-methods"
{/* Show back button only on web, not in Telegram Mini App */}
{!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"
>
<BackIcon />
</Link>
</button>
)}
<h1 className="text-2xl font-bold text-dark-50">
{t('admin.paymentMethods.notFound', 'Payment method not found')}
</h1>
@@ -195,12 +204,15 @@ export default function AdminPaymentMethodEdit() {
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-3">
<Link
to="/admin/payment-methods"
{/* Show back button only on web, not in Telegram Mini App */}
{!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"
>
<BackIcon />
</Link>
</button>
)}
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-dark-700/50 text-xl">
{icon}
</div>

View File

@@ -1,15 +1,35 @@
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { adminPaymentsApi } from '../api/adminPayments';
import { useCurrency } from '../hooks/useCurrency';
import { AdminBackButton } from '../components/admin';
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() {
const { t } = useTranslation();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { formatAmount, currencySymbol } = useCurrency();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
const [page, setPage] = useState(1);
const [methodFilter, setMethodFilter] = useState<string>('');
@@ -68,7 +88,15 @@ export default function AdminPayments() {
{/* Header */}
<div className="flex flex-wrap items-center justify-between gap-4">
<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>
<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>

View File

@@ -9,6 +9,7 @@ import {
PromoGroupUpdateRequest,
} from '../api/promocodes';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
// Icons
const PlusIcon = () => (
@@ -55,6 +56,8 @@ export default function AdminPromoGroupCreate() {
const queryClient = useQueryClient();
const isEdit = !!id;
useBackButton(() => navigate('/admin/promo-groups'));
// Form state
const [name, setName] = useState('');
const [serverDiscount, setServerDiscount] = useState<number | ''>(0);

View File

@@ -3,9 +3,22 @@ import { useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { promocodesApi, PromoGroup } from '../api/promocodes';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
// 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 = () => (
<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" />
@@ -46,6 +59,10 @@ export default function AdminPromoGroups() {
const { t } = useTranslation();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
const [deleteConfirm, setDeleteConfirm] = useState<number | null>(null);
@@ -71,7 +88,15 @@ export default function AdminPromoGroups() {
{/* Header */}
<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">
<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>
<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>

View File

@@ -12,6 +12,7 @@ import {
} from '../api/promoOffers';
import { adminUsersApi, UserListItem } from '../api/adminUsers';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
// Icons
const SendIcon = () => (
@@ -75,6 +76,8 @@ export default function AdminPromoOfferSend() {
const navigate = useNavigate();
const queryClient = useQueryClient();
useBackButton(() => navigate('/admin/promo-offers'));
const [selectedTemplateId, setSelectedTemplateId] = useState<number | null>(null);
const [sendMode, setSendMode] = useState<'segment' | 'user'>('segment');
const [selectedTarget, setSelectedTarget] = useState<TargetSegment>('active');

View File

@@ -10,6 +10,7 @@ import {
} from '../api/promoOffers';
import { AdminBackButton } from '../components/admin';
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
import { useBackButton } from '../platform/hooks/useBackButton';
const getOfferTypeIcon = (offerType: string): string => {
return OFFER_TYPE_CONFIG[offerType as OfferType]?.icon || '🎁';
@@ -21,6 +22,8 @@ export default function AdminPromoOfferTemplateEdit() {
const { id } = useParams<{ id: string }>();
const queryClient = useQueryClient();
useBackButton(() => navigate('/admin/promo-offers'));
// Form state
const [name, setName] = useState('');
const [messageText, setMessageText] = useState('');

View File

@@ -4,9 +4,22 @@ import { useTranslation } from 'react-i18next';
import { useQuery } from '@tanstack/react-query';
import i18n from '../i18n';
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
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 = () => (
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
@@ -93,6 +106,10 @@ const getOfferTypeLabel = (offerType: string): string => {
export default function AdminPromoOffers() {
const { t } = useTranslation();
const navigate = useNavigate();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
const [activeTab, setActiveTab] = useState<'templates' | 'logs'>('templates');
@@ -116,7 +133,15 @@ export default function AdminPromoOffers() {
{/* Header */}
<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">
<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>
<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>

View File

@@ -10,9 +10,22 @@ import {
PromoCodeUpdateRequest,
PromoGroup,
} from '../api/promocodes';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
// 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 = () => (
<svg
className="h-4 w-4 animate-spin"
@@ -34,8 +47,12 @@ export default function AdminPromocodeCreate() {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const queryClient = useQueryClient();
const { capabilities } = usePlatform();
const isEdit = !!id;
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin/promocodes'));
// Form state
const [code, setCode] = useState('');
const [type, setType] = useState<PromoCodeType>('balance');
@@ -186,7 +203,15 @@ export default function AdminPromocodeCreate() {
<div className="space-y-6">
{/* Header */}
<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>
<h1 className="text-xl font-bold text-dark-100">
{isEdit

View File

@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import i18n from '../i18n';
import { promocodesApi, PromoCodeType } from '../api/promocodes';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
// Icons
const EditIcon = () => (
@@ -88,6 +89,8 @@ export default function AdminPromocodeStats() {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
useBackButton(() => navigate('/admin/promocodes'));
const {
data: promocode,
isLoading,

View File

@@ -4,10 +4,23 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import i18n from '../i18n';
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
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 = () => (
<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" />
@@ -98,6 +111,10 @@ export default function AdminPromocodes() {
const { t } = useTranslation();
const navigate = useNavigate();
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 [copiedCode, setCopiedCode] = useState<string | null>(null);
@@ -130,7 +147,15 @@ export default function AdminPromocodes() {
{/* Header */}
<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">
<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>
<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>

View File

@@ -9,7 +9,8 @@ import {
SystemStatsResponse,
AutoSyncStatus,
} from '../api/adminRemnawave';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
import {
ServerIcon,
ChartIcon,
@@ -23,6 +24,20 @@ import {
RemnawaveIcon,
} 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 ============
const formatBytes = (bytes: number): string => {
@@ -861,6 +876,10 @@ export default function AdminRemnawave() {
const { t } = useTranslation();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
// State
const [activeTab, setActiveTab] = useState<TabType>('overview');
@@ -996,7 +1015,15 @@ export default function AdminRemnawave() {
{/* Header */}
<div className="mb-6 flex items-center justify-between">
<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">
<RemnawaveIcon className="h-6 w-6 text-accent-400" />
</div>

View File

@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import { adminRemnawaveApi, SquadWithLocalInfo } from '../api/adminRemnawave';
import { AdminBackButton } from '../components/admin';
import { ServerIcon, UsersIcon, CheckIcon, XIcon } from '../components/icons';
import { useBackButton } from '../platform/hooks/useBackButton';
// Country flags helper
const getCountryFlag = (code: string | null | undefined): string => {
@@ -54,6 +55,8 @@ export default function AdminRemnawaveSquadDetail() {
const { uuid } = useParams<{ uuid: string }>();
const navigate = useNavigate();
useBackButton(() => navigate('/admin/remnawave'));
// Fetch all squads and find the one we need
const {
data: squadsData,

View File

@@ -6,6 +6,7 @@ import { serversApi, ServerUpdateRequest } from '../api/servers';
import { AdminBackButton } from '../components/admin';
import { ServerIcon } from '../components/icons';
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
import { useBackButton } from '../platform/hooks/useBackButton';
// Country flags (simple emoji mapping)
const getCountryFlag = (code: string | null): string => {
@@ -48,6 +49,8 @@ export default function AdminServerEdit() {
const serverId = parseInt(id || '0');
useBackButton(() => navigate('/admin/servers'));
const {
data: server,
isLoading,

View File

@@ -2,8 +2,22 @@ import { useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { serversApi, ServerListItem } from '../api/servers';
import { AdminBackButton } from '../components/admin';
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)
const getCountryFlag = (code: string | null): string => {
@@ -42,6 +56,10 @@ export default function AdminServers() {
const { t } = useTranslation();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
// Queries
const { data: serversData, isLoading } = useQuery({
@@ -79,7 +97,15 @@ export default function AdminServers() {
{/* Header */}
<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">
<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>
<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>

View File

@@ -1,10 +1,13 @@
import { useState, useMemo, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { adminSettingsApi, SettingDefinition } from '../api/adminSettings';
import { themeColorsApi } from '../api/themeColors';
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 { BrandingTab } from '../components/admin/BrandingTab';
import { ThemeTab } from '../components/admin/ThemeTab';
@@ -17,6 +20,19 @@ import {
SettingsSearchResults,
} 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
function findSectionByCategory(categoryKey: string): string | null {
for (const section of MENU_SECTIONS) {
@@ -31,6 +47,11 @@ function findSectionByCategory(categoryKey: string): string | null {
export default function AdminSettings() {
const { t } = useTranslation();
const navigate = useNavigate();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
// State
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="border-b border-dark-700/50 p-4">
<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>
</div>
</div>

View File

@@ -12,6 +12,7 @@ import {
} from '../api/tariffs';
import { AdminBackButton } from '../components/admin';
import { createNumberInputHandler, toNumber } from '../utils/inputHelpers';
import { useBackButton } from '../platform/hooks/useBackButton';
// Icons
const PlusIcon = () => (
@@ -91,6 +92,8 @@ export default function AdminTariffCreate() {
const queryClient = useQueryClient();
const isEdit = !!id;
useBackButton(() => navigate('/admin/tariffs'));
// Step: null = type selection, 'period' or 'daily' = form
const [tariffType, setTariffType] = useState<TariffType>(null);

View File

@@ -2,8 +2,9 @@ import { useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { tariffsApi, TariffListItem } from '../api/tariffs';
import { AdminBackButton } from '../components/admin';
import { useDestructiveConfirm, useNotify } from '@/platform';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
// Icons
const PlusIcon = () => (
@@ -54,12 +55,29 @@ const GiftIcon = () => (
</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() {
const { t } = useTranslation();
const navigate = useNavigate();
const queryClient = useQueryClient();
const confirmDelete = useDestructiveConfirm();
const notify = useNotify();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
// Queries
const { data: tariffsData, isLoading } = useQuery({
@@ -117,7 +135,15 @@ export default function AdminTariffs() {
{/* Header */}
<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">
<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>
<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>

View File

@@ -4,6 +4,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { adminApi } from '../api/admin';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
const SettingsIcon = () => (
<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 queryClient = useQueryClient();
useBackButton(() => navigate('/admin/tickets'));
const {
data: settings,
isLoading,

View File

@@ -4,7 +4,8 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { adminApi, AdminTicket, AdminTicketDetail, AdminTicketMessage } from '../api/admin';
import { ticketsApi } from '../api/tickets';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
function AdminMessageMedia({
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() {
const { t } = useTranslation();
const navigate = useNavigate();
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 [statusFilter, setStatusFilter] = useState<string>('');
const [replyText, setReplyText] = useState('');
@@ -209,7 +228,15 @@ export default function AdminTickets() {
<div className="space-y-6">
<div className="flex items-center justify-between">
<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.tickets.title')}
</h1>

View File

@@ -11,6 +11,7 @@ import {
type UpdateSubscriptionRequest,
} from '../api/adminUsers';
import { AdminBackButton } from '../components/admin';
import { useBackButton } from '../platform/hooks/useBackButton';
// ============ Icons ============
@@ -81,6 +82,8 @@ export default function AdminUserDetail() {
const navigate = useNavigate();
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 locale = localeMap[i18n.language] || 'ru-RU';

View File

@@ -4,10 +4,23 @@ import { useTranslation } from 'react-i18next';
import { useCurrency } from '../hooks/useCurrency';
import { useToast } from '../components/Toast';
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 ============
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 = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
<path
@@ -383,6 +396,10 @@ export default function AdminUsers() {
const { formatWithCurrency } = useCurrency();
const navigate = useNavigate();
const { showToast } = useToast();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
const [users, setUsers] = useState<UserListItem[]>([]);
const [stats, setStats] = useState<UsersStatsResponse | null>(null);
@@ -551,7 +568,15 @@ export default function AdminUsers() {
{/* Header */}
<div className="mb-6 flex items-center justify-between">
<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>
<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>

View File

@@ -1,4 +1,5 @@
import { useState, useCallback, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
@@ -20,12 +21,25 @@ import {
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { adminWheelApi, type WheelPrizeAdmin, type CreateWheelPrizeData } from '../api/wheel';
import { AdminBackButton } from '../components/admin';
import { useDestructiveConfirm } from '@/platform';
import FortuneWheel from '../components/wheel/FortuneWheel';
import { useBackButton } from '../platform/hooks/useBackButton';
import { usePlatform } from '../platform/hooks/usePlatform';
// 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 = () => (
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
<path
@@ -272,8 +286,14 @@ function SortablePrizeCard({
export default function AdminWheel() {
const { t } = useTranslation();
const navigate = useNavigate();
const queryClient = useQueryClient();
const confirmDelete = useDestructiveConfirm();
const { capabilities } = usePlatform();
// Use native Telegram back button in Mini App
useBackButton(() => navigate('/admin'));
const [activeTab, setActiveTab] = useState<Tab>('settings');
const [expandedPrizeId, setExpandedPrizeId] = useState<number | null>(null);
const [isCreating, setIsCreating] = useState(false);
@@ -487,7 +507,15 @@ export default function AdminWheel() {
{/* Header */}
<div className="flex items-center justify-between">
<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>
</div>
<div className="flex items-center gap-2">