mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
Refactor ConnectionModal component: update platform icons to a consistent size, enhance loading and error states with improved UI, and streamline platform detection logic. Add new icons for close, back, copy, check, link, and chevron actions. Optimize layout and styling for better user experience.
This commit is contained in:
@@ -10,40 +10,75 @@ interface ConnectionModalProps {
|
||||
|
||||
// Platform SVG Icons
|
||||
const IosIcon = () => (
|
||||
<svg className="w-8 h-8" viewBox="0 0 24 24" fill="currentColor">
|
||||
<svg className="w-7 h-7" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M18.71 19.5c-.83 1.24-1.71 2.45-3.05 2.47-1.34.03-1.77-.79-3.29-.79-1.53 0-2 .77-3.27.82-1.31.05-2.3-1.32-3.14-2.53C4.25 17 2.94 12.45 4.7 9.39c.87-1.52 2.43-2.48 4.12-2.51 1.28-.02 2.5.87 3.29.87.78 0 2.26-1.07 3.81-.91.65.03 2.47.26 3.64 1.98-.09.06-2.17 1.28-2.15 3.81.03 3.02 2.65 4.03 2.68 4.04-.03.07-.42 1.44-1.38 2.83M13 3.5c.73-.83 1.94-1.46 2.94-1.5.13 1.17-.34 2.35-1.04 3.19-.69.85-1.83 1.51-2.95 1.42-.15-1.15.41-2.35 1.05-3.11z"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const AndroidIcon = () => (
|
||||
<svg className="w-8 h-8" viewBox="0 0 24 24" fill="currentColor">
|
||||
<svg className="w-7 h-7" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M17.6 9.48l1.84-3.18c.16-.31.04-.69-.26-.85-.29-.15-.65-.06-.83.22l-1.88 3.24a11.463 11.463 0 00-8.94 0L5.65 5.67c-.19-.29-.58-.38-.87-.2-.28.18-.37.54-.22.83L6.4 9.48A10.78 10.78 0 003 18h18a10.78 10.78 0 00-3.4-8.52zM7 15.25a1.25 1.25 0 110-2.5 1.25 1.25 0 010 2.5zm10 0a1.25 1.25 0 110-2.5 1.25 1.25 0 010 2.5z"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const WindowsIcon = () => (
|
||||
<svg className="w-8 h-8" viewBox="0 0 24 24" fill="currentColor">
|
||||
<svg className="w-7 h-7" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M3 12V6.75l6-1.32v6.48L3 12zm17-9v8.75l-10 .15V5.21L20 3zM3 13l6 .09v6.81l-6-1.15V13zm17 .25V22l-10-1.91V13.1l10 .15z"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const MacosIcon = () => (
|
||||
<svg className="w-8 h-8" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M4 4h16a2 2 0 012 2v10a2 2 0 01-2 2h-6v2h2a1 1 0 110 2H8a1 1 0 110-2h2v-2H4a2 2 0 01-2-2V6a2 2 0 012-2zm0 2v10h16V6H4zm8 2.5a1 1 0 011 1v3a1 1 0 11-2 0v-3a1 1 0 011-1z"/>
|
||||
<svg className="w-7 h-7" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M4 4h16a2 2 0 012 2v10a2 2 0 01-2 2h-6v2h2a1 1 0 110 2H8a1 1 0 110-2h2v-2H4a2 2 0 01-2-2V6a2 2 0 012-2zm0 2v10h16V6H4z"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const LinuxIcon = () => (
|
||||
<svg className="w-8 h-8" viewBox="0 0 24 24" fill="currentColor">
|
||||
<svg className="w-7 h-7" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C9.5 2 8 4.5 8 7c0 1.5.5 3 1 4-1.5 1-3 3-3 5 0 .5 0 1 .5 1.5-.5.5-1.5 1-1.5 2 0 1.5 2 2.5 4 2.5h6c2 0 4-1 4-2.5 0-1-1-1.5-1.5-2 .5-.5.5-1 .5-1.5 0-2-1.5-4-3-5 .5-1 1-2.5 1-4 0-2.5-1.5-5-4-5zm-2 5c.5 0 1 .5 1 1s-.5 1-1 1-1-.5-1-1 .5-1 1-1zm4 0c.5 0 1 .5 1 1s-.5 1-1 1-1-.5-1-1 .5-1 1-1zm-2 3c1 0 2 .5 2 1s-1 1-2 1-2-.5-2-1 1-1 2-1z"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const TvIcon = () => (
|
||||
<svg className="w-8 h-8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<svg className="w-7 h-7" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<rect x="2" y="4" width="20" height="13" rx="2" ry="2"/>
|
||||
<polyline points="8 21 12 17 16 21"/>
|
||||
<line x1="12" y1="17" x2="12" y2="13"/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
const CloseIcon = () => (
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const BackIcon = () => (
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const CopyIcon = () => (
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const CheckIcon = () => (
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const LinkIcon = () => (
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
const ChevronIcon = () => (
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
@@ -64,72 +99,37 @@ const platformOrder = ['ios', 'android', 'windows', 'macos', 'linux', 'androidTV
|
||||
// Dangerous schemes that should never be allowed
|
||||
const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:']
|
||||
|
||||
/**
|
||||
* Validate URL to prevent XSS via javascript: and other dangerous schemes
|
||||
* Only allows http, https, and known app store URLs
|
||||
*/
|
||||
function isValidExternalUrl(url: string | undefined): boolean {
|
||||
if (!url) return false
|
||||
const lowerUrl = url.toLowerCase().trim()
|
||||
|
||||
// Block dangerous schemes
|
||||
const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:']
|
||||
if (dangerousSchemes.some(scheme => lowerUrl.startsWith(scheme))) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Allow only http/https URLs
|
||||
return lowerUrl.startsWith('http://') || lowerUrl.startsWith('https://')
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate deep link URL - blocks dangerous schemes only
|
||||
* Allows any custom app scheme (clash://, hiddify://, etc.)
|
||||
*/
|
||||
function isValidDeepLink(url: string | undefined): boolean {
|
||||
if (!url) return false
|
||||
const lowerUrl = url.toLowerCase().trim()
|
||||
|
||||
// Block dangerous schemes
|
||||
if (dangerousSchemes.some(scheme => lowerUrl.startsWith(scheme))) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Allow any URL that has a scheme (contains ://)
|
||||
return lowerUrl.includes('://')
|
||||
}
|
||||
|
||||
// Detect user's platform from user agent
|
||||
function detectPlatform(): string | null {
|
||||
if (typeof window === 'undefined' || !navigator?.userAgent) {
|
||||
return null
|
||||
}
|
||||
|
||||
const ua = navigator.userAgent.toLowerCase()
|
||||
|
||||
// Check for mobile devices first
|
||||
if (/iphone|ipad|ipod/.test(ua)) {
|
||||
return 'ios'
|
||||
}
|
||||
if (/iphone|ipad|ipod/.test(ua)) return 'ios'
|
||||
if (/android/.test(ua)) {
|
||||
// Check if it's Android TV
|
||||
if (/tv|television|smart-tv|smarttv/.test(ua)) {
|
||||
return 'androidTV'
|
||||
}
|
||||
if (/tv|television|smart-tv|smarttv/.test(ua)) return 'androidTV'
|
||||
return 'android'
|
||||
}
|
||||
|
||||
// Desktop platforms
|
||||
if (/macintosh|mac os x/.test(ua)) {
|
||||
return 'macos'
|
||||
}
|
||||
if (/windows/.test(ua)) {
|
||||
return 'windows'
|
||||
}
|
||||
if (/linux/.test(ua)) {
|
||||
return 'linux'
|
||||
}
|
||||
|
||||
if (/macintosh|mac os x/.test(ua)) return 'macos'
|
||||
if (/windows/.test(ua)) return 'windows'
|
||||
if (/linux/.test(ua)) return 'linux'
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -145,20 +145,16 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
||||
queryFn: () => subscriptionApi.getAppConfig(),
|
||||
})
|
||||
|
||||
// Auto-detect platform on mount
|
||||
useEffect(() => {
|
||||
const detected = detectPlatform()
|
||||
setDetectedPlatform(detected)
|
||||
setDetectedPlatform(detectPlatform())
|
||||
}, [])
|
||||
|
||||
// Helper to get localized text
|
||||
const getLocalizedText = (text: LocalizedText | undefined): string => {
|
||||
if (!text) return ''
|
||||
const lang = i18n.language || 'en'
|
||||
return text[lang] || text['en'] || text['ru'] || Object.values(text)[0] || ''
|
||||
}
|
||||
|
||||
// Get platform name
|
||||
const getPlatformName = (platformKey: string): string => {
|
||||
if (!appConfig?.platformNames?.[platformKey]) {
|
||||
return platformKey
|
||||
@@ -166,13 +162,11 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
||||
return getLocalizedText(appConfig.platformNames[platformKey])
|
||||
}
|
||||
|
||||
// Get available platforms sorted (detected platform first)
|
||||
const availablePlatforms = useMemo(() => {
|
||||
if (!appConfig?.platforms) return []
|
||||
const available = platformOrder.filter(
|
||||
(key) => appConfig.platforms[key] && appConfig.platforms[key].length > 0
|
||||
)
|
||||
// Move detected platform to the front
|
||||
if (detectedPlatform && available.includes(detectedPlatform)) {
|
||||
const filtered = available.filter(p => p !== detectedPlatform)
|
||||
return [detectedPlatform, ...filtered]
|
||||
@@ -180,13 +174,11 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
||||
return available
|
||||
}, [appConfig, detectedPlatform])
|
||||
|
||||
// Get apps for selected platform
|
||||
const platformApps = useMemo(() => {
|
||||
if (!selectedPlatform || !appConfig?.platforms?.[selectedPlatform]) return []
|
||||
return appConfig.platforms[selectedPlatform]
|
||||
}, [selectedPlatform, appConfig])
|
||||
|
||||
// Copy subscription link
|
||||
const copySubscriptionLink = async () => {
|
||||
if (!appConfig?.subscriptionUrl) return
|
||||
try {
|
||||
@@ -194,7 +186,6 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 2000)
|
||||
} catch {
|
||||
// Fallback for older browsers
|
||||
const textarea = document.createElement('textarea')
|
||||
textarea.value = appConfig.subscriptionUrl
|
||||
document.body.appendChild(textarea)
|
||||
@@ -206,24 +197,16 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle deep link click - use miniapp redirect page like in miniapp index.html
|
||||
const handleConnect = (app: AppInfo) => {
|
||||
// Validate deep link to prevent XSS
|
||||
if (!app.deepLink || !isValidDeepLink(app.deepLink)) {
|
||||
console.warn('Invalid or missing deep link:', app.deepLink)
|
||||
return
|
||||
}
|
||||
|
||||
const lang = i18n.language?.startsWith('ru') ? 'ru' : 'en'
|
||||
const redirectUrl = `${window.location.origin}/miniapp/redirect.html?url=${encodeURIComponent(app.deepLink)}&lang=${lang}`
|
||||
|
||||
// Check if it's a custom URL scheme (not http/https)
|
||||
const isCustomScheme = !/^https?:\/\//i.test(app.deepLink)
|
||||
const tg = (window as any).Telegram?.WebApp
|
||||
|
||||
const tg = (window as unknown as { Telegram?: { WebApp?: { openLink?: (url: string, options?: object) => void } } }).Telegram?.WebApp
|
||||
if (isCustomScheme && tg?.openLink) {
|
||||
// For custom URL schemes - open redirect page in external browser via Telegram
|
||||
// try_browser: true - показывает диалог для перехода во внешний браузер (важно для мобильных)
|
||||
try {
|
||||
tg.openLink(redirectUrl, { try_instant_view: false, try_browser: true })
|
||||
return
|
||||
@@ -231,67 +214,55 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
||||
console.warn('tg.openLink failed:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback - direct navigation
|
||||
window.location.href = redirectUrl
|
||||
}
|
||||
|
||||
// Modal wrapper classes - bottom sheet on mobile, centered on desktop
|
||||
const modalOverlayClass = "fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-end sm:items-center sm:justify-center"
|
||||
const modalCardClass = "w-full sm:max-w-lg sm:mx-4 bg-dark-850 sm:rounded-2xl rounded-t-2xl rounded-b-none border-t border-x sm:border border-dark-700/50 shadow-2xl overflow-hidden"
|
||||
const modalContentClass = "p-4 sm:p-6 pb-[calc(1rem+env(safe-area-inset-bottom,0px))] sm:pb-6"
|
||||
// Allow the sheet to almost fill the viewport height on phones
|
||||
const modalScrollableClass = "h-[calc(100vh-1rem)] sm:h-auto sm:max-h-[85vh]"
|
||||
|
||||
// Loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={modalOverlayClass} onClick={onClose}>
|
||||
<div className={modalCardClass} onClick={(e) => e.stopPropagation()}>
|
||||
<div className={modalContentClass}>
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="w-8 h-8 border-2 border-accent-500 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
<div className="fixed inset-0 bg-black/70 z-50 flex items-end sm:items-center sm:justify-center" onClick={onClose}>
|
||||
<div className="w-full sm:max-w-md sm:mx-4 bg-dark-900 sm:rounded-2xl rounded-t-3xl border-t sm:border border-dark-700/50 p-6" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="w-10 h-10 border-3 border-accent-500 border-t-transparent rounded-full animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (error || !appConfig) {
|
||||
return (
|
||||
<div className={modalOverlayClass} onClick={onClose}>
|
||||
<div className={modalCardClass} onClick={(e) => e.stopPropagation()}>
|
||||
<div className={modalContentClass}>
|
||||
<div className="text-center py-8">
|
||||
<p className="text-error-400">{t('common.error')}</p>
|
||||
<button onClick={onClose} className="btn-secondary mt-4">
|
||||
{t('common.close')}
|
||||
</button>
|
||||
<div className="fixed inset-0 bg-black/70 z-50 flex items-end sm:items-center sm:justify-center" onClick={onClose}>
|
||||
<div className="w-full sm:max-w-md sm:mx-4 bg-dark-900 sm:rounded-2xl rounded-t-3xl border-t sm:border border-dark-700/50 p-6" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="text-center py-8">
|
||||
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-error-500/10 flex items-center justify-center">
|
||||
<span className="text-3xl">😕</span>
|
||||
</div>
|
||||
<p className="text-dark-300 mb-4">{t('common.error')}</p>
|
||||
<button onClick={onClose} className="btn-primary">
|
||||
{t('common.close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// No subscription
|
||||
if (!appConfig.hasSubscription) {
|
||||
return (
|
||||
<div className={modalOverlayClass} onClick={onClose}>
|
||||
<div className={modalCardClass} onClick={(e) => e.stopPropagation()}>
|
||||
<div className={modalContentClass}>
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-lg font-semibold text-dark-100">
|
||||
{t('subscription.connection.title')}
|
||||
</h2>
|
||||
<button onClick={onClose} className="btn-icon">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-center py-8">
|
||||
<p className="text-dark-400">{t('subscription.connection.noSubscription')}</p>
|
||||
<div className="fixed inset-0 bg-black/70 z-50 flex items-end sm:items-center sm:justify-center" onClick={onClose}>
|
||||
<div className="w-full sm:max-w-md sm:mx-4 bg-dark-900 sm:rounded-2xl rounded-t-3xl border-t sm:border border-dark-700/50 p-6" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="text-center py-8">
|
||||
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-warning-500/10 flex items-center justify-center">
|
||||
<span className="text-3xl">📱</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-semibold text-dark-100 mb-2">{t('subscription.connection.title')}</h3>
|
||||
<p className="text-dark-400 mb-4">{t('subscription.connection.noSubscription')}</p>
|
||||
<button onClick={onClose} className="btn-primary">
|
||||
{t('common.close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -301,75 +272,86 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
||||
// Step 1: Select platform
|
||||
if (!selectedPlatform) {
|
||||
return (
|
||||
<div className={modalOverlayClass} onClick={onClose}>
|
||||
<div className={`${modalCardClass} ${modalScrollableClass} flex flex-col animate-slide-up`} onClick={(e) => e.stopPropagation()}>
|
||||
{/* Header - fixed */}
|
||||
<div className="flex justify-between items-center p-4 sm:p-6 pb-0 flex-shrink-0">
|
||||
<h2 className="text-lg font-semibold text-dark-100">
|
||||
{t('subscription.connection.title')}
|
||||
</h2>
|
||||
<button onClick={onClose} className="btn-icon">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<div className="fixed inset-0 bg-black/70 z-50 flex items-end sm:items-center sm:justify-center" onClick={onClose}>
|
||||
<div
|
||||
className="w-full sm:max-w-md sm:mx-4 bg-dark-900 sm:rounded-2xl rounded-t-3xl border-t sm:border border-dark-700/50 max-h-[90vh] flex flex-col animate-slide-up"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-dark-800">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-accent-500/10 flex items-center justify-center">
|
||||
<span className="text-xl">📱</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="font-semibold text-dark-100">{t('subscription.connection.title')}</h2>
|
||||
<p className="text-xs text-dark-500">{t('subscription.connection.selectDevice')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={onClose} className="p-2 rounded-xl hover:bg-dark-800 text-dark-400 hover:text-dark-200 transition-colors">
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Scrollable content */}
|
||||
<div className="flex-1 min-h-0 overflow-y-auto p-4 sm:p-6 pt-3 sm:pt-4">
|
||||
<p className="text-dark-400 text-sm sm:text-base mb-3 sm:mb-4">{t('subscription.connection.selectDevice')}</p>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 sm:gap-3">
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{availablePlatforms.map((platform) => {
|
||||
const IconComponent = platformIconComponents[platform]
|
||||
const isDetected = platform === detectedPlatform
|
||||
return (
|
||||
<button
|
||||
key={platform}
|
||||
onClick={() => setSelectedPlatform(platform)}
|
||||
className={`p-3 sm:p-4 rounded-xl border transition-all text-left group ${
|
||||
platform === detectedPlatform
|
||||
className={`relative p-4 rounded-2xl border-2 transition-all text-left group ${
|
||||
isDetected
|
||||
? 'bg-accent-500/10 border-accent-500/50 hover:border-accent-500'
|
||||
: 'bg-dark-800/50 border-dark-700 hover:border-accent-500/50 hover:bg-dark-700/50'
|
||||
: 'bg-dark-800/50 border-dark-700/50 hover:border-dark-600 hover:bg-dark-800'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className={`${
|
||||
platform === detectedPlatform ? 'text-accent-400' : 'text-dark-400 group-hover:text-dark-200'
|
||||
} transition-colors`}>
|
||||
{IconComponent && <IconComponent />}
|
||||
{isDetected && (
|
||||
<div className="absolute -top-2 -right-2 px-2 py-0.5 rounded-full text-[10px] font-medium bg-accent-500 text-white">
|
||||
{t('subscription.connection.yourDevice')}
|
||||
</div>
|
||||
{platform === detectedPlatform && (
|
||||
<span className="px-2 py-0.5 rounded-full text-xs bg-accent-500/20 text-accent-400">
|
||||
{t('subscription.connection.yourDevice')}
|
||||
</span>
|
||||
)}
|
||||
)}
|
||||
<div className={`mb-3 ${isDetected ? 'text-accent-400' : 'text-dark-400 group-hover:text-dark-300'} transition-colors`}>
|
||||
{IconComponent && <IconComponent />}
|
||||
</div>
|
||||
<p className="text-dark-200 font-medium mt-2">{getPlatformName(platform)}</p>
|
||||
<p className={`font-medium ${isDetected ? 'text-accent-300' : 'text-dark-200'}`}>
|
||||
{getPlatformName(platform)}
|
||||
</p>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer - fixed with safe area */}
|
||||
<div className="flex-shrink-0 p-4 sm:p-6 pt-3 sm:pt-4 pb-[calc(1rem+env(safe-area-inset-bottom,0px))] sm:pb-6 border-t border-dark-700/50">
|
||||
{/* Footer */}
|
||||
<div className="p-4 border-t border-dark-800 pb-[calc(1rem+env(safe-area-inset-bottom,0px))] sm:pb-4">
|
||||
<button
|
||||
onClick={copySubscriptionLink}
|
||||
className="w-full p-2.5 sm:p-3 rounded-xl bg-dark-800/50 border border-dark-700 hover:border-accent-500/50 transition-all flex items-center justify-center gap-2 text-sm sm:text-base"
|
||||
className={`w-full p-4 rounded-2xl border-2 border-dashed transition-all flex items-center justify-center gap-3 ${
|
||||
copied
|
||||
? 'border-success-500/50 bg-success-500/10'
|
||||
: 'border-dark-700 hover:border-accent-500/50 hover:bg-dark-800/50'
|
||||
}`}
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<svg className="w-5 h-5 text-success-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-success-400">{t('subscription.connection.copied')}</span>
|
||||
<div className="w-8 h-8 rounded-full bg-success-500/20 flex items-center justify-center">
|
||||
<CheckIcon />
|
||||
</div>
|
||||
<span className="font-medium text-success-400">{t('subscription.connection.copied')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span className="text-dark-300">{t('subscription.connection.copyLink')}</span>
|
||||
<div className="w-8 h-8 rounded-full bg-dark-700 flex items-center justify-center text-dark-400">
|
||||
<CopyIcon />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<p className="font-medium text-dark-200">{t('subscription.connection.copyLink')}</p>
|
||||
<p className="text-xs text-dark-500">Для ручного подключения</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -379,62 +361,65 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
||||
)
|
||||
}
|
||||
|
||||
// Step 2: Select app (if not selected yet)
|
||||
// Step 2: Select app
|
||||
if (!selectedApp) {
|
||||
return (
|
||||
<div className={modalOverlayClass} onClick={onClose}>
|
||||
<div className={`${modalCardClass} ${modalScrollableClass} flex flex-col animate-slide-up`} onClick={(e) => e.stopPropagation()}>
|
||||
{/* Header - fixed */}
|
||||
<div className="flex justify-between items-center p-4 sm:p-6 pb-0 flex-shrink-0">
|
||||
<div className="fixed inset-0 bg-black/70 z-50 flex items-end sm:items-center sm:justify-center" onClick={onClose}>
|
||||
<div
|
||||
className="w-full sm:max-w-md sm:mx-4 bg-dark-900 sm:rounded-2xl rounded-t-3xl border-t sm:border border-dark-700/50 max-h-[90vh] flex flex-col animate-slide-up"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-dark-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setSelectedPlatform(null)}
|
||||
className="btn-icon"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
<button onClick={() => setSelectedPlatform(null)} className="p-2 -ml-2 rounded-xl hover:bg-dark-800 text-dark-400 hover:text-dark-200 transition-colors">
|
||||
<BackIcon />
|
||||
</button>
|
||||
<h2 className="text-lg font-semibold text-dark-100">
|
||||
{getPlatformName(selectedPlatform)}
|
||||
</h2>
|
||||
<div>
|
||||
<h2 className="font-semibold text-dark-100">{getPlatformName(selectedPlatform)}</h2>
|
||||
<p className="text-xs text-dark-500">{t('subscription.connection.selectApp')}</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={onClose} className="btn-icon">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<button onClick={onClose} className="p-2 rounded-xl hover:bg-dark-800 text-dark-400 hover:text-dark-200 transition-colors">
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Scrollable content */}
|
||||
<div className="flex-1 min-h-0 overflow-y-auto p-4 sm:p-6 pt-3 sm:pt-4 pb-[calc(1rem+env(safe-area-inset-bottom,0px))] sm:pb-6">
|
||||
<p className="text-dark-400 text-sm sm:text-base mb-3 sm:mb-4">{t('subscription.connection.selectApp')}</p>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4 pb-[calc(1rem+env(safe-area-inset-bottom,0px))] sm:pb-4">
|
||||
{platformApps.length === 0 ? (
|
||||
<div className="text-center py-6 sm:py-8">
|
||||
<div className="text-center py-12">
|
||||
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-dark-800 flex items-center justify-center">
|
||||
<span className="text-3xl">📭</span>
|
||||
</div>
|
||||
<p className="text-dark-500">{t('subscription.connection.noApps')}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 sm:space-y-3">
|
||||
<div className="space-y-2">
|
||||
{platformApps.map((app) => (
|
||||
<button
|
||||
key={app.id}
|
||||
onClick={() => setSelectedApp(app)}
|
||||
className="w-full p-3 sm:p-4 rounded-xl bg-dark-800/50 border border-dark-700 hover:border-accent-500/50 hover:bg-dark-700/50 transition-all text-left flex items-center justify-between"
|
||||
className="w-full p-4 rounded-2xl bg-dark-800/50 border border-dark-700/50 hover:border-dark-600 hover:bg-dark-800 transition-all flex items-center justify-between group"
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-dark-100 font-medium">{app.name}</span>
|
||||
{app.isFeatured && (
|
||||
<span className="px-2 py-0.5 rounded-full text-xs bg-accent-500/20 text-accent-400">
|
||||
{t('subscription.connection.featured')}
|
||||
</span>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-dark-700 flex items-center justify-center text-lg">
|
||||
{app.isFeatured ? '⭐' : '📦'}
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-dark-100">{app.name}</span>
|
||||
{app.isFeatured && (
|
||||
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-accent-500/20 text-accent-400">
|
||||
{t('subscription.connection.featured')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<svg className="w-5 h-5 text-dark-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
<div className="text-dark-500 group-hover:text-dark-300 transition-colors">
|
||||
<ChevronIcon />
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -445,46 +430,44 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
||||
)
|
||||
}
|
||||
|
||||
// Step 3: Show app instructions and connect button
|
||||
// Step 3: App instructions
|
||||
return (
|
||||
<div className={modalOverlayClass} onClick={onClose}>
|
||||
<div className={`${modalCardClass} ${modalScrollableClass} flex flex-col animate-slide-up`} onClick={(e) => e.stopPropagation()}>
|
||||
{/* Header - fixed */}
|
||||
<div className="flex justify-between items-center p-4 sm:p-6 pb-0 flex-shrink-0">
|
||||
<div className="fixed inset-0 bg-black/70 z-50 flex items-end sm:items-center sm:justify-center" onClick={onClose}>
|
||||
<div
|
||||
className="w-full sm:max-w-md sm:mx-4 bg-dark-900 sm:rounded-2xl rounded-t-3xl border-t sm:border border-dark-700/50 max-h-[90vh] flex flex-col animate-slide-up"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-dark-800">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setSelectedApp(null)}
|
||||
className="btn-icon"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
<button onClick={() => setSelectedApp(null)} className="p-2 -ml-2 rounded-xl hover:bg-dark-800 text-dark-400 hover:text-dark-200 transition-colors">
|
||||
<BackIcon />
|
||||
</button>
|
||||
<h2 className="text-lg font-semibold text-dark-100">
|
||||
{selectedApp.name}
|
||||
</h2>
|
||||
<div>
|
||||
<h2 className="font-semibold text-dark-100">{selectedApp.name}</h2>
|
||||
<p className="text-xs text-dark-500">Инструкция по подключению</p>
|
||||
</div>
|
||||
</div>
|
||||
<button onClick={onClose} className="btn-icon">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<button onClick={onClose} className="p-2 rounded-xl hover:bg-dark-800 text-dark-400 hover:text-dark-200 transition-colors">
|
||||
<CloseIcon />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Scrollable content */}
|
||||
<div className="flex-1 min-h-0 overflow-y-auto p-4 sm:p-6 pt-3 sm:pt-4">
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<div className="space-y-4">
|
||||
{/* Step 1: Install */}
|
||||
{selectedApp.installationStep && (
|
||||
<div className="space-y-2 sm:space-y-3">
|
||||
<h3 className="text-sm font-medium text-accent-400">
|
||||
{t('subscription.connection.installApp')}
|
||||
</h3>
|
||||
<p className="text-sm text-dark-400">
|
||||
<div className="p-4 rounded-2xl bg-dark-800/50 border border-dark-700/50">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-8 h-8 rounded-full bg-accent-500/20 flex items-center justify-center text-sm font-bold text-accent-400">1</div>
|
||||
<h3 className="font-medium text-dark-100">{t('subscription.connection.installApp')}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-dark-400 mb-3 pl-11">
|
||||
{getLocalizedText(selectedApp.installationStep.description)}
|
||||
</p>
|
||||
{selectedApp.installationStep.buttons && selectedApp.installationStep.buttons.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex flex-wrap gap-2 pl-11">
|
||||
{selectedApp.installationStep.buttons
|
||||
.filter((btn) => isValidExternalUrl(btn.buttonLink))
|
||||
.map((btn, idx) => (
|
||||
@@ -495,9 +478,7 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-dark-700 text-dark-200 text-sm hover:bg-dark-600 transition-all"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
||||
</svg>
|
||||
<LinkIcon />
|
||||
{getLocalizedText(btn.buttonText)}
|
||||
</a>
|
||||
))}
|
||||
@@ -506,88 +487,53 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Additional before add subscription */}
|
||||
{selectedApp.additionalBeforeAddSubscriptionStep && (
|
||||
<div className="space-y-2 p-3 rounded-xl bg-dark-800/50 border border-dark-700">
|
||||
{selectedApp.additionalBeforeAddSubscriptionStep.title && (
|
||||
<h4 className="text-sm font-medium text-dark-200">
|
||||
{getLocalizedText(selectedApp.additionalBeforeAddSubscriptionStep.title)}
|
||||
</h4>
|
||||
)}
|
||||
<p className="text-xs text-dark-400">
|
||||
{getLocalizedText(selectedApp.additionalBeforeAddSubscriptionStep.description)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Add subscription */}
|
||||
{selectedApp.addSubscriptionStep && (
|
||||
<div className="space-y-2 sm:space-y-3">
|
||||
<h3 className="text-sm font-medium text-accent-400">
|
||||
{t('subscription.connection.addSubscription')}
|
||||
</h3>
|
||||
<p className="text-sm text-dark-400">
|
||||
<div className="p-4 rounded-2xl bg-dark-800/50 border border-dark-700/50">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-8 h-8 rounded-full bg-accent-500/20 flex items-center justify-center text-sm font-bold text-accent-400">2</div>
|
||||
<h3 className="font-medium text-dark-100">{t('subscription.connection.addSubscription')}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-dark-400 mb-4 pl-11">
|
||||
{getLocalizedText(selectedApp.addSubscriptionStep.description)}
|
||||
</p>
|
||||
|
||||
{/* Connect button */}
|
||||
{selectedApp.deepLink && (
|
||||
<button
|
||||
onClick={() => handleConnect(selectedApp)}
|
||||
className="btn-primary w-full py-2.5 sm:py-3 flex items-center justify-center gap-2 text-sm sm:text-base"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25" />
|
||||
</svg>
|
||||
{t('subscription.connection.addToApp', { appName: selectedApp.name })}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Copy link fallback */}
|
||||
<button
|
||||
onClick={copySubscriptionLink}
|
||||
className="w-full p-2.5 sm:p-3 rounded-xl bg-dark-800/50 border border-dark-700 hover:border-accent-500/50 transition-all flex items-center justify-center gap-2 text-sm sm:text-base"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<svg className="w-5 h-5 text-success-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-success-400">{t('subscription.connection.copied')}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-5 h-5 text-dark-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span className="text-dark-300">{t('subscription.connection.copyLink')}</span>
|
||||
</>
|
||||
<div className="space-y-2 pl-11">
|
||||
{/* Connect button */}
|
||||
{selectedApp.deepLink && (
|
||||
<button
|
||||
onClick={() => handleConnect(selectedApp)}
|
||||
className="btn-primary w-full py-3 flex items-center justify-center gap-2"
|
||||
>
|
||||
<LinkIcon />
|
||||
{t('subscription.connection.addToApp', { appName: selectedApp.name })}
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Additional after add subscription */}
|
||||
{selectedApp.additionalAfterAddSubscriptionStep && (
|
||||
<div className="space-y-2 p-3 rounded-xl bg-dark-800/50 border border-dark-700">
|
||||
{selectedApp.additionalAfterAddSubscriptionStep.title && (
|
||||
<h4 className="text-sm font-medium text-dark-200">
|
||||
{getLocalizedText(selectedApp.additionalAfterAddSubscriptionStep.title)}
|
||||
</h4>
|
||||
)}
|
||||
<p className="text-xs text-dark-400">
|
||||
{getLocalizedText(selectedApp.additionalAfterAddSubscriptionStep.description)}
|
||||
</p>
|
||||
{/* Copy link */}
|
||||
<button
|
||||
onClick={copySubscriptionLink}
|
||||
className={`w-full p-3 rounded-xl border transition-all flex items-center justify-center gap-2 ${
|
||||
copied
|
||||
? 'border-success-500/50 bg-success-500/10 text-success-400'
|
||||
: 'border-dark-700 hover:border-dark-600 bg-dark-800/50 text-dark-300'
|
||||
}`}
|
||||
>
|
||||
{copied ? <CheckIcon /> : <CopyIcon />}
|
||||
{copied ? t('subscription.connection.copied') : t('subscription.connection.copyLink')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Connect */}
|
||||
{selectedApp.connectAndUseStep && (
|
||||
<div className="space-y-2 sm:space-y-3">
|
||||
<h3 className="text-sm font-medium text-accent-400">
|
||||
{t('subscription.connection.connectVpn')}
|
||||
</h3>
|
||||
<p className="text-sm text-dark-400">
|
||||
<div className="p-4 rounded-2xl bg-dark-800/50 border border-dark-700/50">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="w-8 h-8 rounded-full bg-success-500/20 flex items-center justify-center text-sm font-bold text-success-400">3</div>
|
||||
<h3 className="font-medium text-dark-100">{t('subscription.connection.connectVpn')}</h3>
|
||||
</div>
|
||||
<p className="text-sm text-dark-400 pl-11">
|
||||
{getLocalizedText(selectedApp.connectAndUseStep.description)}
|
||||
</p>
|
||||
</div>
|
||||
@@ -595,9 +541,9 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer - fixed with safe area */}
|
||||
<div className="flex-shrink-0 p-4 sm:p-6 pt-3 sm:pt-4 pb-[calc(1rem+env(safe-area-inset-bottom,0px))] sm:pb-6 border-t border-dark-700/50">
|
||||
<button onClick={onClose} className="btn-secondary w-full text-sm sm:text-base">
|
||||
{/* Footer */}
|
||||
<div className="p-4 border-t border-dark-800 pb-[calc(1rem+env(safe-area-inset-bottom,0px))] sm:pb-4">
|
||||
<button onClick={onClose} className="btn-secondary w-full">
|
||||
{t('common.close')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user