mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
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:
@@ -1218,6 +1218,7 @@
|
||||
"cannotDeleteHasSubscriptions": "Cannot delete: has active subscriptions",
|
||||
"confirmDelete": "Delete tariff?",
|
||||
"confirmDeleteText": "This action cannot be undone. The tariff will be permanently deleted.",
|
||||
"deleteSuccess": "Tariff deleted successfully",
|
||||
"days": "days",
|
||||
"days_one": "{{count}} day",
|
||||
"days_other": "{{count}} days",
|
||||
|
||||
@@ -992,6 +992,7 @@
|
||||
"cannotDeleteHasSubscriptions": "حذف ممکن نیست: اشتراکهای فعال وجود دارد",
|
||||
"confirmDelete": "تعرفه حذف شود؟",
|
||||
"confirmDeleteText": "این عمل قابل بازگشت نیست. تعرفه برای همیشه حذف میشود.",
|
||||
"deleteSuccess": "تعرفه با موفقیت حذف شد",
|
||||
"days": "روز",
|
||||
"days_other": "{{count}} روز",
|
||||
"tabs": {
|
||||
|
||||
@@ -1728,6 +1728,7 @@
|
||||
"cannotDeleteHasSubscriptions": "Нельзя удалить: есть активные подписки",
|
||||
"confirmDelete": "Удалить тариф?",
|
||||
"confirmDeleteText": "Это действие нельзя отменить. Тариф будет удален навсегда.",
|
||||
"deleteSuccess": "Тариф успешно удален",
|
||||
"days": "дней",
|
||||
"days_one": "{{count}} день",
|
||||
"days_few": "{{count}} дня",
|
||||
|
||||
@@ -1030,6 +1030,7 @@
|
||||
"cannotDeleteHasSubscriptions": "无法删除:有活跃订阅",
|
||||
"confirmDelete": "删除套餐?",
|
||||
"confirmDeleteText": "此操作不可撤销。套餐将被永久删除。",
|
||||
"deleteSuccess": "套餐删除成功",
|
||||
"days": "天",
|
||||
"days_other": "{{count}} 天",
|
||||
"tabs": {
|
||||
|
||||
@@ -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'),
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
92
src/platform/hooks/useNotify.ts
Normal file
92
src/platform/hooks/useNotify.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useCallback } from 'react';
|
||||
import { usePlatform } from '@/platform/hooks/usePlatform';
|
||||
import { useToast } from '@/components/Toast';
|
||||
|
||||
type NotifyType = 'success' | 'error' | 'warning' | 'info';
|
||||
|
||||
interface NotifyOptions {
|
||||
type?: NotifyType;
|
||||
title?: string;
|
||||
message: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Universal notification hook.
|
||||
* - In Telegram Mini App: uses native showPopup
|
||||
* - In browser: uses Toast component
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const notify = useNotify();
|
||||
*
|
||||
* // Warning notification
|
||||
* notify.warning('Cannot delete: has active subscriptions');
|
||||
*
|
||||
* // Success notification
|
||||
* notify.success('Tariff created successfully');
|
||||
*
|
||||
* // With title
|
||||
* notify({ type: 'error', title: 'Error', message: 'Something went wrong' });
|
||||
* ```
|
||||
*/
|
||||
export function useNotify() {
|
||||
const { dialog, capabilities } = usePlatform();
|
||||
const { showToast } = useToast();
|
||||
|
||||
const notify = useCallback(
|
||||
(options: NotifyOptions | string) => {
|
||||
const opts: NotifyOptions = typeof options === 'string' ? { message: options } : options;
|
||||
|
||||
const { type = 'info', title, message, duration } = opts;
|
||||
|
||||
// In Telegram - use native popup for important messages
|
||||
if (capabilities.hasNativeDialogs) {
|
||||
dialog.popup({
|
||||
title: title,
|
||||
message: message,
|
||||
buttons: [{ id: 'ok', type: 'ok', text: 'OK' }],
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// In browser - use Toast
|
||||
showToast({
|
||||
type,
|
||||
title,
|
||||
message,
|
||||
duration: duration ?? (type === 'error' ? 7000 : 5000),
|
||||
});
|
||||
},
|
||||
[capabilities.hasNativeDialogs, dialog, showToast],
|
||||
);
|
||||
|
||||
// Shorthand methods
|
||||
const success = useCallback(
|
||||
(message: string, title?: string) => notify({ type: 'success', message, title }),
|
||||
[notify],
|
||||
);
|
||||
|
||||
const error = useCallback(
|
||||
(message: string, title?: string) => notify({ type: 'error', message, title }),
|
||||
[notify],
|
||||
);
|
||||
|
||||
const warning = useCallback(
|
||||
(message: string, title?: string) => notify({ type: 'warning', message, title }),
|
||||
[notify],
|
||||
);
|
||||
|
||||
const info = useCallback(
|
||||
(message: string, title?: string) => notify({ type: 'info', message, title }),
|
||||
[notify],
|
||||
);
|
||||
|
||||
return {
|
||||
notify,
|
||||
success,
|
||||
error,
|
||||
warning,
|
||||
info,
|
||||
};
|
||||
}
|
||||
@@ -31,3 +31,4 @@ export { useBackButton, useConditionalBackButton } from './hooks/useBackButton';
|
||||
export { useMainButton, useSimpleMainButton } from './hooks/useMainButton';
|
||||
export { useHaptic, useHapticClick, useHapticFeedback } from './hooks/useHaptic';
|
||||
export { useNativeDialog, useDestructiveConfirm, PopupButtons } from './hooks/useNativeDialog';
|
||||
export { useNotify } from './hooks/useNotify';
|
||||
|
||||
Reference in New Issue
Block a user