- {/* Badge button */}
-
-
- {/* Dropdown - mobile: fixed centered, desktop: absolute right */}
- {isOpen && (
- <>
- {/* Mobile backdrop */}
-
setIsOpen(false)}
- />
-
-
- {/* Header */}
-
-
-
-
-
-
-
{t('promo.discountActive')}
-
- -{activeDiscount.discount_percent}%
-
-
-
-
-
- {/* Content */}
-
-
{t('promo.discountDescription')}
-
- {/* Time remaining */}
- {timeLeft && (
-
-
-
- {t('promo.expiresIn')}:{' '}
- {timeLeft}
-
-
- )}
-
- {/* Deactivation error */}
- {deactivateError && (
-
- {deactivateError}
-
- )}
-
- {/* CTA Button */}
-
-
- {/* Deactivate Button */}
-
-
-
- >
- )}
-
-
- {/* Deactivation Confirmation Modal */}
-
- >
- );
-}
diff --git a/src/components/PromoOffersSection.tsx b/src/components/PromoOffersSection.tsx
index f14ee55..b0a3ac4 100644
--- a/src/components/PromoOffersSection.tsx
+++ b/src/components/PromoOffersSection.tsx
@@ -1,7 +1,7 @@
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
-import { useNavigate } from 'react-router-dom';
+import { useNavigate } from 'react-router';
import { promoApi, PromoOffer } from '../api/promo';
import { ClockIcon, CheckIcon } from './icons';
import { usePlatform } from '@/platform/hooks/usePlatform';
diff --git a/src/components/SuccessNotificationModal.tsx b/src/components/SuccessNotificationModal.tsx
index dcb4c45..a97b49a 100644
--- a/src/components/SuccessNotificationModal.tsx
+++ b/src/components/SuccessNotificationModal.tsx
@@ -6,11 +6,11 @@
import { useEffect, useCallback } from 'react';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
-import { useNavigate } from 'react-router-dom';
+import { useNavigate } from 'react-router';
import { useSuccessNotification } from '../store/successNotification';
import { useCurrency } from '../hooks/useCurrency';
-import { useTelegramWebApp } from '../hooks/useTelegramWebApp';
-import { useBackButton, useHaptic } from '@/platform';
+import { useTelegramSDK } from '../hooks/useTelegramSDK';
+import { useHaptic } from '@/platform';
// Icons
const CheckCircleIcon = () => (
@@ -80,7 +80,7 @@ export default function SuccessNotificationModal() {
const navigate = useNavigate();
const { isOpen, data, hide } = useSuccessNotification();
const { formatAmount, currencySymbol } = useCurrency();
- const { safeAreaInset, contentSafeAreaInset, isTelegramWebApp } = useTelegramWebApp();
+ const { safeAreaInset, contentSafeAreaInset, isTelegramWebApp } = useTelegramSDK();
const haptic = useHaptic();
const safeBottom = isTelegramWebApp
@@ -106,9 +106,6 @@ export default function SuccessNotificationModal() {
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isOpen, handleClose]);
- // Telegram back button - using platform hook
- useBackButton(isOpen ? handleClose : null);
-
// Haptic feedback on open
useEffect(() => {
if (isOpen) {
diff --git a/src/components/ThemeBentoPicker.tsx b/src/components/ThemeBentoPicker.tsx
deleted file mode 100644
index dadc957..0000000
--- a/src/components/ThemeBentoPicker.tsx
+++ /dev/null
@@ -1,579 +0,0 @@
-import { useState, useEffect, useCallback, useMemo } from 'react';
-import { useTranslation } from 'react-i18next';
-import { COLOR_PRESETS, ColorPreset } from '../data/colorPresets';
-import { hexToHsl, hslToHex, isValidHex, HSLColor } from '../utils/colorConversion';
-import { ThemeColors, DEFAULT_THEME_COLORS } from '../types/theme';
-import { applyThemeColors } from '../hooks/useThemeColors';
-
-interface ThemeBentoPickerProps {
- currentColors: ThemeColors;
- onColorsChange: (colors: ThemeColors) => void;
- onSave: () => void;
- isSaving: boolean;
-}
-
-const CheckIcon = () => (
-
-);
-
-const ChevronDownIcon = () => (
-
-);
-
-const MoonIcon = () => (
-
-);
-
-const SunIcon = () => (
-
-);
-
-const StatusIcon = () => (
-
-);
-
-const PaletteIcon = () => (
-
-);
-
-function PresetCard({
- preset,
- isSelected,
- onClick,
-}: {
- preset: ColorPreset;
- isSelected: boolean;
- onClick: () => void;
-}) {
- const { i18n } = useTranslation();
- const isRu = i18n.language === 'ru';
-
- return (
-
- );
-}
-
-function HSLSlider({
- label,
- value,
- onChange,
- max,
- gradient,
- suffix = '',
-}: {
- label: string;
- value: number;
- onChange: (value: number) => void;
- max: number;
- gradient: string;
- suffix?: string;
-}) {
- return (
-
-
-
-
- {value}
- {suffix}
-
-
-
onChange(parseInt(e.target.value))}
- className="h-2.5 w-full cursor-pointer appearance-none rounded-full"
- style={{ background: gradient }}
- />
-
- );
-}
-
-function CompactColorInput({
- label,
- value,
- onChange,
-}: {
- label: string;
- value: string;
- onChange: (color: string) => void;
-}) {
- const [localValue, setLocalValue] = useState(value);
- const [isEditing, setIsEditing] = useState(false);
-
- useEffect(() => {
- setLocalValue(value);
- }, [value]);
-
- const handleChange = (newValue: string) => {
- let formatted = newValue.toUpperCase();
- if (!formatted.startsWith('#')) {
- formatted = '#' + formatted;
- }
- setLocalValue(formatted);
- if (isValidHex(formatted)) {
- onChange(formatted);
- }
- };
-
- const handleBlur = () => {
- setIsEditing(false);
- if (!isValidHex(localValue)) {
- setLocalValue(value);
- }
- };
-
- return (
-
- );
-}
-
-function CollapsibleSection({
- title,
- icon,
- isOpen,
- onToggle,
- children,
- badge,
-}: {
- title: string;
- icon: React.ReactNode;
- isOpen: boolean;
- onToggle: () => void;
- children: React.ReactNode;
- badge?: string;
-}) {
- return (
-
-
-
-
-
- );
-}
-
-export function ThemeBentoPicker({
- currentColors,
- onColorsChange,
- onSave,
- isSaving,
-}: ThemeBentoPickerProps) {
- const { t } = useTranslation();
-
- const [hsl, setHsl] = useState
(() => hexToHsl(currentColors.accent));
- const [hexInput, setHexInput] = useState(currentColors.accent);
- const [hasChanges, setHasChanges] = useState(false);
-
- const [isPresetsOpen, setIsPresetsOpen] = useState(false);
- const [isAccentOpen, setIsAccentOpen] = useState(false);
- const [isDarkOpen, setIsDarkOpen] = useState(false);
- const [isLightOpen, setIsLightOpen] = useState(false);
- const [isStatusOpen, setIsStatusOpen] = useState(false);
-
- const selectedPresetId = useMemo(() => {
- const match = COLOR_PRESETS.find(
- (p) =>
- p.colors.accent.toLowerCase() === currentColors.accent.toLowerCase() &&
- p.colors.darkBackground.toLowerCase() === currentColors.darkBackground.toLowerCase() &&
- p.colors.lightBackground.toLowerCase() === currentColors.lightBackground.toLowerCase(),
- );
- return match?.id ?? null;
- }, [currentColors.accent, currentColors.darkBackground, currentColors.lightBackground]);
-
- useEffect(() => {
- setHsl(hexToHsl(currentColors.accent));
- setHexInput(currentColors.accent);
- }, [currentColors.accent]);
-
- const updateColor = useCallback(
- (key: keyof ThemeColors, value: string) => {
- const newColors = { ...currentColors, [key]: value };
- onColorsChange(newColors);
- applyThemeColors(newColors);
- setHasChanges(true);
- },
- [currentColors, onColorsChange],
- );
-
- const updateAccentFromHsl = useCallback(
- (newHsl: HSLColor) => {
- setHsl(newHsl);
- const newHex = hslToHex(newHsl.h, newHsl.s, newHsl.l);
- setHexInput(newHex);
- updateColor('accent', newHex);
- },
- [updateColor],
- );
-
- const handleHexInputChange = (value: string) => {
- setHexInput(value);
- if (isValidHex(value)) {
- const newHsl = hexToHsl(value);
- setHsl(newHsl);
- updateColor('accent', value);
- }
- };
-
- const handlePresetSelect = (preset: ColorPreset) => {
- onColorsChange(preset.colors);
- applyThemeColors(preset.colors);
- setHasChanges(true);
- };
-
- const hueGradient = useMemo(() => {
- return `linear-gradient(to right,
- hsl(0, ${hsl.s}%, ${hsl.l}%),
- hsl(60, ${hsl.s}%, ${hsl.l}%),
- hsl(120, ${hsl.s}%, ${hsl.l}%),
- hsl(180, ${hsl.s}%, ${hsl.l}%),
- hsl(240, ${hsl.s}%, ${hsl.l}%),
- hsl(300, ${hsl.s}%, ${hsl.l}%),
- hsl(360, ${hsl.s}%, ${hsl.l}%)
- )`;
- }, [hsl.s, hsl.l]);
-
- const saturationGradient = useMemo(() => {
- return `linear-gradient(to right,
- hsl(${hsl.h}, 0%, ${hsl.l}%),
- hsl(${hsl.h}, 100%, ${hsl.l}%)
- )`;
- }, [hsl.h, hsl.l]);
-
- const lightnessGradient = useMemo(() => {
- return `linear-gradient(to right,
- hsl(${hsl.h}, ${hsl.s}%, 0%),
- hsl(${hsl.h}, ${hsl.s}%, 50%),
- hsl(${hsl.h}, ${hsl.s}%, 100%)
- )`;
- }, [hsl.h, hsl.s]);
-
- return (
-
-
}
- badge={`${COLOR_PRESETS.length}`}
- isOpen={isPresetsOpen}
- onToggle={() => setIsPresetsOpen(!isPresetsOpen)}
- >
-
- {COLOR_PRESETS.map((preset, index) => (
-
-
handlePresetSelect(preset)}
- />
-
- ))}
-
-
-
-
-
- {t('admin.theme.customizeColors', 'Customize Colors')}
-
-
-
}
- badge={hexInput.toUpperCase()}
- isOpen={isAccentOpen}
- onToggle={() => setIsAccentOpen(!isAccentOpen)}
- >
-
-
-
-
- {hexInput.toUpperCase()}
-
-
-
-
updateAccentFromHsl({ ...hsl, h })}
- max={360}
- gradient={hueGradient}
- suffix="Β°"
- />
-
- updateAccentFromHsl({ ...hsl, s })}
- max={100}
- gradient={saturationGradient}
- suffix="%"
- />
-
- updateAccentFromHsl({ ...hsl, l })}
- max={100}
- gradient={lightnessGradient}
- suffix="%"
- />
-
-
-
- handleHexInputChange(e.target.value)}
- placeholder="#3b82f6"
- maxLength={7}
- className="input w-full font-mono text-sm uppercase"
- />
-
-
-
-
-
}
- isOpen={isDarkOpen}
- onToggle={() => setIsDarkOpen(!isDarkOpen)}
- >
-
- updateColor('darkBackground', c)}
- />
- updateColor('darkSurface', c)}
- />
- updateColor('darkText', c)}
- />
- updateColor('darkTextSecondary', c)}
- />
-
-
-
-
}
- isOpen={isLightOpen}
- onToggle={() => setIsLightOpen(!isLightOpen)}
- >
-
- updateColor('lightBackground', c)}
- />
- updateColor('lightSurface', c)}
- />
- updateColor('lightText', c)}
- />
- updateColor('lightTextSecondary', c)}
- />
-
-
-
-
}
- isOpen={isStatusOpen}
- onToggle={() => setIsStatusOpen(!isStatusOpen)}
- >
-
- updateColor('success', c)}
- />
- updateColor('warning', c)}
- />
- updateColor('error', c)}
- />
-
-
-
-
-
-
- {t('theme.preview', 'Preview')}
-
-
-
-
- {t('theme.success', 'Success')}
- {t('theme.warning', 'Warning')}
- {t('theme.error', 'Error')}
-
-
-
- {hasChanges && (
-
-
-
- )}
-
- );
-}
diff --git a/src/components/TicketNotificationBell.tsx b/src/components/TicketNotificationBell.tsx
index c44a6f7..693f58d 100644
--- a/src/components/TicketNotificationBell.tsx
+++ b/src/components/TicketNotificationBell.tsx
@@ -1,12 +1,12 @@
import { useState, useRef, useEffect, useCallback } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
-import { useNavigate } from 'react-router-dom';
+import { useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next';
import { ticketNotificationsApi } from '../api/ticketNotifications';
import { useAuthStore } from '../store/auth';
import { useToast } from './Toast';
import { useWebSocket, WSMessage } from '../hooks/useWebSocket';
-import { useTelegramWebApp } from '../hooks/useTelegramWebApp';
+import { useTelegramSDK } from '../hooks/useTelegramSDK';
import type { TicketNotification } from '../types';
const BellIcon = () => (
@@ -37,7 +37,7 @@ export default function TicketNotificationBell({ isAdmin = false }: TicketNotifi
const { showToast } = useToast();
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
- const { isFullscreen, safeAreaInset, contentSafeAreaInset } = useTelegramWebApp();
+ const { isFullscreen, safeAreaInset, contentSafeAreaInset } = useTelegramSDK();
// Calculate dropdown top position (account for fullscreen safe area + TG buttons)
const dropdownTop = isFullscreen
diff --git a/src/components/WebSocketNotifications.tsx b/src/components/WebSocketNotifications.tsx
index ea8cc70..3baffb3 100644
--- a/src/components/WebSocketNotifications.tsx
+++ b/src/components/WebSocketNotifications.tsx
@@ -4,7 +4,7 @@
*/
import { useCallback } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next';
import { useQueryClient } from '@tanstack/react-query';
import { useWebSocket, WSMessage } from '../hooks/useWebSocket';
diff --git a/src/components/admin/AdminBackButton.tsx b/src/components/admin/AdminBackButton.tsx
index 7972d13..5dc084b 100644
--- a/src/components/admin/AdminBackButton.tsx
+++ b/src/components/admin/AdminBackButton.tsx
@@ -1,4 +1,4 @@
-import { Link } from 'react-router-dom';
+import { Link } from 'react-router';
import { usePlatform } from '@/platform';
import { BackIcon } from './icons';
diff --git a/src/components/admin/AdminLayout.tsx b/src/components/admin/AdminLayout.tsx
deleted file mode 100644
index 526f5ae..0000000
--- a/src/components/admin/AdminLayout.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import type { ReactNode } from 'react';
-
-interface AdminLayoutProps {
- children: ReactNode;
- className?: string;
-}
-
-/**
- * AdminLayout - wrapper for all admin pages.
- * Animations removed to prevent black flash during transitions.
- */
-export function AdminLayout({ children, className }: AdminLayoutProps) {
- return {children}
;
-}
diff --git a/src/components/admin/BrandingTab.tsx b/src/components/admin/BrandingTab.tsx
index bcdd233..915fa29 100644
--- a/src/components/admin/BrandingTab.tsx
+++ b/src/components/admin/BrandingTab.tsx
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { brandingApi, setCachedBranding } from '../../api/branding';
import { setCachedAnimationEnabled } from '../AnimatedBackground';
-import { setCachedFullscreenEnabled } from '../../hooks/useTelegramWebApp';
+import { setCachedFullscreenEnabled } from '../../hooks/useTelegramSDK';
import { UploadIcon, TrashIcon, PencilIcon, CheckIcon, CloseIcon } from './icons';
import { Toggle } from './Toggle';
diff --git a/src/components/admin/SettingsSidebar.tsx b/src/components/admin/SettingsSidebar.tsx
deleted file mode 100644
index 5236c00..0000000
--- a/src/components/admin/SettingsSidebar.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import { useTranslation } from 'react-i18next';
-import { AdminBackButton, StarIcon, CloseIcon, MENU_SECTIONS } from './index';
-
-interface SettingsSidebarProps {
- activeSection: string;
- setActiveSection: (section: string) => void;
- mobileMenuOpen: boolean;
- setMobileMenuOpen: (open: boolean) => void;
- favoritesCount: number;
-}
-
-export function SettingsSidebar({
- activeSection,
- setActiveSection,
- mobileMenuOpen,
- setMobileMenuOpen,
- favoritesCount,
-}: SettingsSidebarProps) {
- const { t } = useTranslation();
-
- return (
-
- );
-}
diff --git a/src/components/admin/ThemeTab.tsx b/src/components/admin/ThemeTab.tsx
index 380d851..da9f2f3 100644
--- a/src/components/admin/ThemeTab.tsx
+++ b/src/components/admin/ThemeTab.tsx
@@ -59,6 +59,8 @@ export function ThemeTab() {
// Local draft state
const [draftColors, setDraftColors] = useState(DEFAULT_THEME_COLORS);
const savedColorsRef = useRef(DEFAULT_THEME_COLORS);
+ const draftColorsRef = useRef(draftColors);
+ draftColorsRef.current = draftColors;
// Sync server data into draft and saved snapshot when it arrives
useEffect(() => {
@@ -79,51 +81,17 @@ export function ThemeTab() {
};
// Only sync if saved snapshot matches current draft (no unsaved changes)
if (
- colorsEqual(savedColorsRef.current, draftColors) ||
+ colorsEqual(savedColorsRef.current, draftColorsRef.current) ||
colorsEqual(savedColorsRef.current, DEFAULT_THEME_COLORS)
) {
setDraftColors(colors);
}
savedColorsRef.current = colors;
}
- }, [serverColors]); // eslint-disable-line react-hooks/exhaustive-deps
+ }, [serverColors]);
const hasUnsavedChanges = !colorsEqual(draftColors, savedColorsRef.current);
- // Update a single color in the draft and apply preview instantly
- const updateDraftColor = useCallback(
- (key: keyof ThemeColors, value: string) => {
- setDraftColors((prev) => {
- const next = { ...prev, [key]: value };
- applyThemeColors(next);
- queryClient.setQueryData(['theme-colors'], next);
- return next;
- });
- },
- [queryClient],
- );
-
- // Apply a full preset to draft
- const applyPreset = useCallback(
- (colors: Partial) => {
- setDraftColors((prev) => {
- const next = { ...prev, ...colors };
- applyThemeColors(next);
- queryClient.setQueryData(['theme-colors'], next);
- return next;
- });
- },
- [queryClient],
- );
-
- // Cancel: revert draft to saved
- const handleCancel = useCallback(() => {
- const saved = savedColorsRef.current;
- setDraftColors(saved);
- applyThemeColors(saved);
- queryClient.setQueryData(['theme-colors'], saved);
- }, [queryClient]);
-
// Mutations
const updateColorsMutation = useMutation({
mutationFn: themeColorsApi.updateColors,
@@ -181,6 +149,42 @@ export function ThemeTab() {
},
});
+ // Update a single color in the draft and apply preview instantly
+ const updateDraftColor = useCallback(
+ (key: keyof ThemeColors, value: string) => {
+ setDraftColors((prev) => {
+ const next = { ...prev, [key]: value };
+ applyThemeColors(next);
+ queryClient.setQueryData(['theme-colors'], next);
+ return next;
+ });
+ },
+ [queryClient],
+ );
+
+ // Apply a full preset and auto-save to server
+ const applyPreset = useCallback(
+ (colors: Partial) => {
+ setDraftColors((prev) => {
+ const next = { ...prev, ...colors };
+ applyThemeColors(next);
+ queryClient.setQueryData(['theme-colors'], next);
+ // Auto-save preset to server so it persists across navigation
+ updateColorsMutation.mutate(next);
+ return next;
+ });
+ },
+ [queryClient, updateColorsMutation],
+ );
+
+ // Cancel: revert draft to saved
+ const handleCancel = useCallback(() => {
+ const saved = savedColorsRef.current;
+ setDraftColors(saved);
+ applyThemeColors(saved);
+ queryClient.setQueryData(['theme-colors'], saved);
+ }, [queryClient]);
+
return (
{/* Theme toggles */}
@@ -414,17 +418,21 @@ export function ThemeTab() {
>
)}
-
)}
+
+ {/* Reset all colors */}
+