feat: add useNotify hook for unified notifications

- Created useNotify hook (src/platform/hooks/useNotify.ts):
  - In Telegram Mini App: uses native showPopup
  - In browser: uses beautiful Toast component
  - Methods: notify(), success(), error(), warning(), info()

- AdminTariffs: improved deletion UX
  - Uses useNotify.warning when tariff has subscriptions
  - Uses useDestructiveConfirm for delete confirmation
  - Shows success toast after deletion
  - Removed custom modal dialog

- AdminEmailTemplates: unified notification style
  - Replaced dialog.alert with useNotify.warning
  - Now shows beautiful Toast on mobile browser

- Added deleteSuccess i18n translations (ru, en, zh, fa)
This commit is contained in:
c0mrade
2026-02-02 02:17:19 +03:00
parent 5b30f24e7e
commit 6f4d1ef085
8 changed files with 125 additions and 49 deletions

View File

@@ -9,7 +9,8 @@ import {
EmailTemplateLanguageData,
} from '../api/adminEmailTemplates';
import { AdminBackButton, BackIcon } from '../components/admin';
import { useIsTelegram, usePlatform } from '../platform/hooks/usePlatform';
import { useIsTelegram } from '../platform/hooks/usePlatform';
import { useNotify } from '@/platform';
// Hook to check if on mobile
function useIsMobile() {
@@ -159,7 +160,7 @@ function TemplateEditor({
const { t } = useTranslation();
const navigate = useNavigate();
const queryClient = useQueryClient();
const { dialog } = usePlatform();
const notify = useNotify();
const isTelegram = useIsTelegram();
const isMobile = useIsMobile();
const isPreviewDisabled = isTelegram || isMobile;
@@ -401,7 +402,7 @@ function TemplateEditor({
<button
onClick={() => {
if (isPreviewDisabled) {
dialog.alert(
notify.warning(
t('admin.emailTemplates.previewDesktopOnly'),
t('admin.emailTemplates.previewNotAvailable'),
);

View File

@@ -1,9 +1,9 @@
import { useState } from 'react';
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';
// Icons
const PlusIcon = () => (
@@ -48,8 +48,8 @@ export default function AdminTariffs() {
const { t } = useTranslation();
const navigate = useNavigate();
const queryClient = useQueryClient();
const [deleteConfirm, setDeleteConfirm] = useState<number | null>(null);
const confirmDelete = useDestructiveConfirm();
const notify = useNotify();
// Queries
const { data: tariffsData, isLoading } = useQuery({
@@ -62,10 +62,27 @@ export default function AdminTariffs() {
mutationFn: tariffsApi.deleteTariff,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['admin-tariffs'] });
setDeleteConfirm(null);
notify.success(t('admin.tariffs.deleteSuccess'));
},
});
const handleDelete = async (tariff: TariffListItem) => {
if (tariff.subscriptions_count > 0) {
notify.warning(t('admin.tariffs.cannotDeleteHasSubscriptions'));
return;
}
const confirmed = await confirmDelete(
t('admin.tariffs.confirmDeleteText'),
t('common.delete'),
t('admin.tariffs.confirmDelete'),
);
if (confirmed) {
deleteMutation.mutate(tariff.id);
}
};
const toggleMutation = useMutation({
mutationFn: tariffsApi.toggleTariff,
onSuccess: () => {
@@ -204,18 +221,9 @@ export default function AdminTariffs() {
{/* Delete */}
<button
onClick={() => setDeleteConfirm(tariff.id)}
className={`rounded-lg p-2 transition-colors ${
tariff.subscriptions_count > 0
? 'cursor-not-allowed bg-dark-700/50 text-dark-500'
: 'bg-dark-700 text-dark-300 hover:bg-error-500/20 hover:text-error-400'
}`}
title={
tariff.subscriptions_count > 0
? t('admin.tariffs.cannotDeleteHasSubscriptions')
: t('admin.tariffs.delete')
}
disabled={tariff.subscriptions_count > 0}
onClick={() => handleDelete(tariff)}
className="rounded-lg bg-dark-700 p-2 text-dark-300 transition-colors hover:bg-error-500/20 hover:text-error-400"
title={t('admin.tariffs.delete')}
>
<TrashIcon />
</button>
@@ -225,36 +233,6 @@ export default function AdminTariffs() {
))}
</div>
)}
{/* Delete Confirmation */}
{deleteConfirm !== null && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={() => setDeleteConfirm(null)}
/>
<div className="relative w-full max-w-sm rounded-xl bg-dark-800 p-6">
<h3 className="mb-2 text-lg font-semibold text-dark-100">
{t('admin.tariffs.confirmDelete')}
</h3>
<p className="mb-6 text-dark-400">{t('admin.tariffs.confirmDeleteText')}</p>
<div className="flex justify-end gap-3">
<button
onClick={() => setDeleteConfirm(null)}
className="px-4 py-2 text-dark-300 transition-colors hover:text-dark-100"
>
{t('common.cancel')}
</button>
<button
onClick={() => deleteMutation.mutate(deleteConfirm)}
className="rounded-lg bg-error-500 px-4 py-2 text-white transition-colors hover:bg-error-600"
>
{t('common.delete')}
</button>
</div>
</div>
</div>
)}
</div>
);
}