mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
fix: adapt referral network for Telegram MiniApp safe areas
- Extract useHeaderHeight() hook — single source of truth for header height across AppShell, ReferralNetwork, and TicketNotificationBell - Portal top offset now computed dynamically from TG safe area insets instead of hardcoded top-16 (fixes overlap with TG header bar) - Two-row mobile layout: title + selector on separate rows so dropdown gets full width on small screens - Add MOBILE_HEADER_HEIGHT_PX and DESKTOP_HEADER_HEIGHT_PX to constants - Fix TicketNotificationBell using hardcoded 45px for all platforms
This commit is contained in:
@@ -6,7 +6,7 @@ import { ticketNotificationsApi } from '../api/ticketNotifications';
|
|||||||
import { useAuthStore } from '../store/auth';
|
import { useAuthStore } from '../store/auth';
|
||||||
import { useToast } from './Toast';
|
import { useToast } from './Toast';
|
||||||
import { useWebSocket, WSMessage } from '../hooks/useWebSocket';
|
import { useWebSocket, WSMessage } from '../hooks/useWebSocket';
|
||||||
import { useTelegramSDK } from '../hooks/useTelegramSDK';
|
import { useHeaderHeight } from '../hooks/useHeaderHeight';
|
||||||
import type { TicketNotification } from '../types';
|
import type { TicketNotification } from '../types';
|
||||||
|
|
||||||
const BellIcon = () => (
|
const BellIcon = () => (
|
||||||
@@ -37,12 +37,7 @@ export default function TicketNotificationBell({ isAdmin = false }: TicketNotifi
|
|||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const { isFullscreen, safeAreaInset, contentSafeAreaInset } = useTelegramSDK();
|
const { mobile: dropdownTop } = useHeaderHeight();
|
||||||
|
|
||||||
// Calculate dropdown top position (account for fullscreen safe area + TG buttons)
|
|
||||||
const dropdownTop = isFullscreen
|
|
||||||
? Math.max(safeAreaInset.top, contentSafeAreaInset.top) + 45 + 64 // safe area + TG buttons + header
|
|
||||||
: 64; // default header height
|
|
||||||
|
|
||||||
// Show toast for WebSocket notification
|
// Show toast for WebSocket notification
|
||||||
const showWSNotificationToast = useCallback(
|
const showWSNotificationToast = useCallback(
|
||||||
@@ -259,10 +254,8 @@ export default function TicketNotificationBell({ isAdmin = false }: TicketNotifi
|
|||||||
{/* Dropdown */}
|
{/* Dropdown */}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div
|
<div
|
||||||
className={`fixed left-4 right-4 z-50 mt-0 w-auto animate-scale-in overflow-hidden rounded-2xl border border-dark-700/50 bg-dark-900/95 shadow-2xl shadow-black/30 backdrop-blur-xl sm:absolute sm:left-auto sm:right-0 sm:top-auto sm:mt-2 sm:w-96 ${
|
className="fixed left-4 right-4 z-50 mt-0 w-auto animate-scale-in overflow-hidden rounded-2xl border border-dark-700/50 bg-dark-900/95 shadow-2xl shadow-black/30 backdrop-blur-xl sm:absolute sm:left-auto sm:right-0 sm:top-auto sm:mt-2 sm:w-96"
|
||||||
!isFullscreen ? 'top-16' : ''
|
style={{ top: dropdownTop }}
|
||||||
}`}
|
|
||||||
style={isFullscreen ? { top: `${dropdownTop}px` } : undefined}
|
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between border-b border-dark-700/50 bg-dark-800/30 px-4 py-3">
|
<div className="flex items-center justify-between border-b border-dark-700/50 bg-dark-800/30 px-4 py-3">
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useAuthStore } from '@/store/auth';
|
import { useAuthStore } from '@/store/auth';
|
||||||
import { useHaptic } from '@/platform';
|
import { useHaptic } from '@/platform';
|
||||||
import { useTelegramSDK } from '@/hooks/useTelegramSDK';
|
import { useTelegramSDK } from '@/hooks/useTelegramSDK';
|
||||||
|
import { useHeaderHeight } from '@/hooks/useHeaderHeight';
|
||||||
import { useTheme } from '@/hooks/useTheme';
|
import { useTheme } from '@/hooks/useTheme';
|
||||||
import { useBranding } from '@/hooks/useBranding';
|
import { useBranding } from '@/hooks/useBranding';
|
||||||
import { useFeatureFlags } from '@/hooks/useFeatureFlags';
|
import { useFeatureFlags } from '@/hooks/useFeatureFlags';
|
||||||
@@ -13,7 +14,6 @@ import { useScrollRestoration } from '@/hooks/useScrollRestoration';
|
|||||||
import { themeColorsApi } from '@/api/themeColors';
|
import { themeColorsApi } from '@/api/themeColors';
|
||||||
import { isLogoPreloaded } from '@/api/branding';
|
import { isLogoPreloaded } from '@/api/branding';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { UI } from '@/config/constants';
|
|
||||||
|
|
||||||
import WebSocketNotifications from '@/components/WebSocketNotifications';
|
import WebSocketNotifications from '@/components/WebSocketNotifications';
|
||||||
import CampaignBonusNotifier from '@/components/CampaignBonusNotifier';
|
import CampaignBonusNotifier from '@/components/CampaignBonusNotifier';
|
||||||
@@ -198,6 +198,7 @@ export function AppShell({ children }: AppShellProps) {
|
|||||||
const logout = useAuthStore((state) => state.logout);
|
const logout = useAuthStore((state) => state.logout);
|
||||||
const { isFullscreen, safeAreaInset, contentSafeAreaInset, platform, isMobile } =
|
const { isFullscreen, safeAreaInset, contentSafeAreaInset, platform, isMobile } =
|
||||||
useTelegramSDK();
|
useTelegramSDK();
|
||||||
|
const { mobile: headerHeight } = useHeaderHeight();
|
||||||
const haptic = useHaptic();
|
const haptic = useHaptic();
|
||||||
const { toggleTheme, isDark } = useTheme();
|
const { toggleTheme, isDark } = useTheme();
|
||||||
|
|
||||||
@@ -269,14 +270,7 @@ export function AppShell({ children }: AppShellProps) {
|
|||||||
haptic.impact('light');
|
haptic.impact('light');
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate header height based on fullscreen mode (only on mobile Telegram)
|
// headerHeight comes from useHeaderHeight() — accounts for TG safe area in fullscreen
|
||||||
// On iOS: contentSafeAreaInset.top includes status bar + dynamic island + Telegram header
|
|
||||||
// On Android: safeAreaInset.top only includes status bar, need to add Telegram header height (~48px)
|
|
||||||
const telegramHeaderHeight =
|
|
||||||
platform === 'android' ? UI.TELEGRAM_HEADER_ANDROID_PX : UI.TELEGRAM_HEADER_IOS_PX;
|
|
||||||
const headerHeight = isMobileFullscreen
|
|
||||||
? 64 + Math.max(safeAreaInset.top, contentSafeAreaInset.top) + telegramHeaderHeight
|
|
||||||
: 64;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export const UI = {
|
|||||||
RESEND_COOLDOWN_SEC: 60,
|
RESEND_COOLDOWN_SEC: 60,
|
||||||
TELEGRAM_HEADER_ANDROID_PX: 48,
|
TELEGRAM_HEADER_ANDROID_PX: 48,
|
||||||
TELEGRAM_HEADER_IOS_PX: 45,
|
TELEGRAM_HEADER_IOS_PX: 45,
|
||||||
|
MOBILE_HEADER_HEIGHT_PX: 64,
|
||||||
|
DESKTOP_HEADER_HEIGHT_PX: 56,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
// API
|
// API
|
||||||
|
|||||||
25
src/hooks/useHeaderHeight.ts
Normal file
25
src/hooks/useHeaderHeight.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { useTelegramSDK } from '@/hooks/useTelegramSDK';
|
||||||
|
import { UI } from '@/config/constants';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computes the app header height in pixels, accounting for
|
||||||
|
* Telegram MiniApp safe area insets in fullscreen mode.
|
||||||
|
*
|
||||||
|
* Desktop: 56px (h-14). Mobile: 64px (h-16) + safe area + TG header when fullscreen.
|
||||||
|
*/
|
||||||
|
export function useHeaderHeight(): { mobile: number; desktop: number } {
|
||||||
|
const { isFullscreen, safeAreaInset, contentSafeAreaInset, platform, isMobile } =
|
||||||
|
useTelegramSDK();
|
||||||
|
const isMobileFullscreen = isFullscreen && isMobile;
|
||||||
|
|
||||||
|
const telegramHeaderHeight =
|
||||||
|
platform === 'android' ? UI.TELEGRAM_HEADER_ANDROID_PX : UI.TELEGRAM_HEADER_IOS_PX;
|
||||||
|
|
||||||
|
const mobile = isMobileFullscreen
|
||||||
|
? UI.MOBILE_HEADER_HEIGHT_PX +
|
||||||
|
Math.max(safeAreaInset.top, contentSafeAreaInset.top) +
|
||||||
|
telegramHeaderHeight
|
||||||
|
: UI.MOBILE_HEADER_HEIGHT_PX;
|
||||||
|
|
||||||
|
return { mobile, desktop: UI.DESKTOP_HEADER_HEIGHT_PX };
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { referralNetworkApi } from '@/api/referralNetwork';
|
import { referralNetworkApi } from '@/api/referralNetwork';
|
||||||
import { useReferralNetworkStore } from '@/store/referralNetwork';
|
import { useReferralNetworkStore } from '@/store/referralNetwork';
|
||||||
|
import { useHeaderHeight } from '@/hooks/useHeaderHeight';
|
||||||
import { AdminBackButton } from '@/components/admin/AdminBackButton';
|
import { AdminBackButton } from '@/components/admin/AdminBackButton';
|
||||||
import { NetworkGraph } from './components/NetworkGraph';
|
import { NetworkGraph } from './components/NetworkGraph';
|
||||||
import { ScopeSelector } from './components/ScopeSelector';
|
import { ScopeSelector } from './components/ScopeSelector';
|
||||||
@@ -20,6 +21,8 @@ export function ReferralNetwork() {
|
|||||||
const removeScope = useReferralNetworkStore((s) => s.removeScope);
|
const removeScope = useReferralNetworkStore((s) => s.removeScope);
|
||||||
const clearScope = useReferralNetworkStore((s) => s.clearScope);
|
const clearScope = useReferralNetworkStore((s) => s.clearScope);
|
||||||
|
|
||||||
|
const { mobile: mobileHeaderHeight } = useHeaderHeight();
|
||||||
|
|
||||||
const hasScope = scope.length > 0;
|
const hasScope = scope.length > 0;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -38,14 +41,18 @@ export function ReferralNetwork() {
|
|||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
id="referral-network-container"
|
id="referral-network-container"
|
||||||
className="fixed inset-x-0 bottom-0 top-16 z-40 grid grid-rows-[auto_1fr] bg-[#0a0a0f] lg:top-14"
|
className="fixed inset-x-0 bottom-0 z-40 grid grid-rows-[auto_1fr] bg-[#0a0a0f] lg:!top-14"
|
||||||
|
style={{ top: mobileHeaderHeight }}
|
||||||
>
|
>
|
||||||
<div className="z-20 border-b border-dark-700/50 bg-dark-900/90 backdrop-blur-md">
|
<div className="z-20 border-b border-dark-700/50 bg-dark-900/90 backdrop-blur-md">
|
||||||
<div className="flex items-center gap-2 px-3 py-2 sm:gap-3 sm:px-4 sm:py-3">
|
{/* Mobile: two rows — title on top, selector below */}
|
||||||
<AdminBackButton />
|
<div className="flex flex-col gap-1.5 px-3 py-2 sm:flex-row sm:items-center sm:gap-3 sm:px-4 sm:py-3">
|
||||||
<h1 className="shrink-0 text-sm font-bold text-dark-100 sm:text-base">
|
<div className="flex items-center gap-2">
|
||||||
{t('admin.referralNetwork.title')}
|
<AdminBackButton />
|
||||||
</h1>
|
<h1 className="shrink-0 text-sm font-bold text-dark-100 sm:text-base">
|
||||||
|
{t('admin.referralNetwork.title')}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
<ScopeSelector
|
<ScopeSelector
|
||||||
value={scope}
|
value={scope}
|
||||||
onAdd={addScope}
|
onAdd={addScope}
|
||||||
|
|||||||
Reference in New Issue
Block a user