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 { 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>

View File

@@ -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');

View File

@@ -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,

View File

@@ -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>

View File

@@ -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('');

View File

@@ -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,

View File

@@ -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,

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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');

View File

@@ -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('');

View File

@@ -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>

View File

@@ -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

View File

@@ -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,

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,

View File

@@ -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,

View File

@@ -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>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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>

View File

@@ -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,

View File

@@ -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>

View File

@@ -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';

View File

@@ -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>

View File

@@ -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">