From 3cea48235f373412071afbb7d811a2306ad15b78 Mon Sep 17 00:00:00 2001 From: Fringg Date: Fri, 6 Mar 2026 07:23:11 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20=D0=B1=D0=B5=D0=B7=D0=BE=D0=BF=D0=B0?= =?UTF-8?q?=D1=81=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=B8=20UX=20=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B4=D0=B8=D0=BD=D0=B3=D0=BE=D0=B2=20=E2=80=94=20?= =?UTF-8?q?16=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CRITICAL: усиленная CSS-санитизация (escape-последовательности, at-rules) - CRITICAL: очистка setTimeout при unmount - HIGH: useMutation для создания покупки (вместо ручного fetch) - HIGH: DOMPurify + rel="noopener noreferrer" на ссылках - HIGH: ErrorBoundary для публичных страниц покупки - MEDIUM: стабильные UUID для DnD элементов - MEDIUM: race condition в delete timeout - MEDIUM: клиентская валидация формы лендинга - MEDIUM: staleTime для админ-запросов - MEDIUM: общая утилита getApiErrorMessage - MEDIUM: уведомление о таймауте поллинга - LOW: ARIA роли для radio-карточек и форм - LOW: дедупликация иконок в LandingIcons.tsx - LOW: cn() вместо template literals --- src/App.tsx | 17 ++- src/components/icons/LandingIcons.tsx | 69 +++++++++++ src/locales/en.json | 11 +- src/locales/fa.json | 11 +- src/locales/ru.json | 11 +- src/locales/zh.json | 11 +- src/pages/AdminLandingEditor.tsx | 165 +++++++++++++------------- src/pages/AdminLandings.tsx | 78 ++++-------- src/pages/PurchaseSuccess.tsx | 74 +++++++++++- src/pages/QuickPurchase.tsx | 151 ++++++++++++++--------- src/utils/api-error.ts | 8 ++ 11 files changed, 389 insertions(+), 217 deletions(-) create mode 100644 src/components/icons/LandingIcons.tsx create mode 100644 src/utils/api-error.ts diff --git a/src/App.tsx b/src/App.tsx index 404dc99..7b70b76 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,6 +9,7 @@ import { ChannelSubscriptionScreen, BlacklistedScreen, } from './components/blocking'; +import { ErrorBoundary } from './components/ErrorBoundary'; import { PermissionRoute } from '@/components/auth/PermissionRoute'; import { saveReturnUrl } from './utils/token'; import { useAnalyticsCounters } from './hooks/useAnalyticsCounters'; @@ -201,17 +202,21 @@ function App() { - - + + + + + } /> - - + + + + + } /> diff --git a/src/components/icons/LandingIcons.tsx b/src/components/icons/LandingIcons.tsx new file mode 100644 index 0000000..378d3e4 --- /dev/null +++ b/src/components/icons/LandingIcons.tsx @@ -0,0 +1,69 @@ +import { cn } from '../../lib/utils'; + +interface IconProps { + className?: string; +} + +export function BackIcon({ className }: IconProps) { + return ( + + + + ); +} + +export function PlusIcon({ className }: IconProps) { + return ( + + + + ); +} + +export function TrashIcon({ className }: IconProps) { + return ( + + + + ); +} + +export function GripIcon({ className }: IconProps) { + return ( + + + + ); +} diff --git a/src/locales/en.json b/src/locales/en.json index ec55915..8922cc4 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -3184,7 +3184,11 @@ "general": "General", "content": "Content", "save": "Save", - "back": "Back" + "back": "Back", + "invalidSlug": "Slug can only contain lowercase letters, numbers, and hyphens", + "titleRequired": "Title is required", + "noTariffs": "Select at least one tariff", + "noPaymentMethods": "Add at least one payment method" } }, "adminUpdates": { @@ -3950,6 +3954,9 @@ "pay": "Pay", "choosePeriod": "Choose period", "copied": "Copied!", - "copyLink": "Copy link" + "copyLink": "Copy link", + "giftToggleLabel": "Purchase type", + "pollTimedOut": "Taking longer than expected", + "pollTimedOutDesc": "Payment processing is taking longer than usual. You can try checking again." } } diff --git a/src/locales/fa.json b/src/locales/fa.json index bfe612a..82cf1ee 100644 --- a/src/locales/fa.json +++ b/src/locales/fa.json @@ -2910,7 +2910,11 @@ "general": "عمومی", "content": "محتوا", "save": "ذخیره", - "back": "بازگشت" + "back": "بازگشت", + "invalidSlug": "Slug can only contain lowercase letters, numbers, and hyphens", + "titleRequired": "Title is required", + "noTariffs": "Select at least one tariff", + "noPaymentMethods": "Add at least one payment method" } }, "adminUpdates": { @@ -3501,6 +3505,9 @@ "pay": "پرداخت", "choosePeriod": "انتخاب دوره", "copied": "کپی شد!", - "copyLink": "کپی لینک" + "copyLink": "کپی لینک", + "giftToggleLabel": "Purchase type", + "pollTimedOut": "Taking longer than expected", + "pollTimedOutDesc": "Payment processing is taking longer than usual. You can try checking again." } } diff --git a/src/locales/ru.json b/src/locales/ru.json index feeda29..250e706 100644 --- a/src/locales/ru.json +++ b/src/locales/ru.json @@ -3739,7 +3739,11 @@ "general": "Основное", "content": "Контент", "save": "Сохранить", - "back": "Назад" + "back": "Назад", + "invalidSlug": "Slug может содержать только строчные буквы, цифры и дефисы", + "titleRequired": "Заголовок обязателен", + "noTariffs": "Выберите хотя бы один тариф", + "noPaymentMethods": "Добавьте хотя бы один способ оплаты" } }, "adminUpdates": { @@ -4513,6 +4517,9 @@ "pay": "Оплатить", "choosePeriod": "Выберите период", "copied": "Скопировано!", - "copyLink": "Скопировать ссылку" + "copyLink": "Скопировать ссылку", + "giftToggleLabel": "Тип покупки", + "pollTimedOut": "Занимает больше времени, чем ожидалось", + "pollTimedOutDesc": "Обработка платежа занимает больше времени, чем обычно. Вы можете попробовать проверить ещё раз." } } diff --git a/src/locales/zh.json b/src/locales/zh.json index 7665974..ac80a4d 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -2909,7 +2909,11 @@ "general": "基本设置", "content": "内容", "save": "保存", - "back": "返回" + "back": "返回", + "invalidSlug": "Slug can only contain lowercase letters, numbers, and hyphens", + "titleRequired": "Title is required", + "noTariffs": "Select at least one tariff", + "noPaymentMethods": "Add at least one payment method" } }, "adminUpdates": { @@ -3500,6 +3504,9 @@ "pay": "支付", "choosePeriod": "选择时长", "copied": "已复制!", - "copyLink": "复制链接" + "copyLink": "复制链接", + "giftToggleLabel": "Purchase type", + "pollTimedOut": "Taking longer than expected", + "pollTimedOutDesc": "Payment processing is taking longer than usual. You can try checking again." } } diff --git a/src/pages/AdminLandingEditor.tsx b/src/pages/AdminLandingEditor.tsx index a6cf745..a714087 100644 --- a/src/pages/AdminLandingEditor.tsx +++ b/src/pages/AdminLandingEditor.tsx @@ -12,6 +12,8 @@ import { tariffsApi, TariffListItem } from '../api/tariffs'; import { Toggle } from '../components/admin'; import { useNotify } from '@/platform'; import { usePlatform } from '../platform/hooks/usePlatform'; +import { getApiErrorMessage } from '../utils/api-error'; +import { BackIcon, PlusIcon, TrashIcon, GripIcon } from '../components/icons/LandingIcons'; import { DndContext, KeyboardSensor, @@ -28,49 +30,15 @@ import { verticalListSortingStrategy, } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; +import { cn } from '../lib/utils'; -// Icons -const BackIcon = () => ( - - - -); - -const PlusIcon = () => ( - - - -); - -const TrashIcon = () => ( - - - -); - -const GripIcon = () => ( - - - -); +// Types with stable IDs for DnD +type FeatureWithId = LandingFeature & { _id: string }; +type MethodWithId = LandingPaymentMethod & { _id: string }; const ChevronDownIcon = ({ open }: { open: boolean }) => ( ( // ============ Sortable Feature Item ============ interface SortableFeatureProps { - feature: LandingFeature; - featureId: string; + feature: FeatureWithId; index: number; onUpdate: (index: number, field: keyof LandingFeature, value: string) => void; onRemove: (index: number) => void; } -function SortableFeatureItem({ - feature, - featureId, - index, - onUpdate, - onRemove, -}: SortableFeatureProps) { +function SortableFeatureItem({ feature, index, onUpdate, onRemove }: SortableFeatureProps) { const { t } = useTranslation(); const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ - id: featureId, + id: feature._id, }); const style: React.CSSProperties = { @@ -113,9 +74,10 @@ function SortableFeatureItem({