mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +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
|
// Platform SVG Icons
|
||||||
const IosIcon = () => (
|
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"/>
|
<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>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const AndroidIcon = () => (
|
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"/>
|
<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>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const WindowsIcon = () => (
|
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"/>
|
<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>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const MacosIcon = () => (
|
const MacosIcon = () => (
|
||||||
<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="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"/>
|
<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>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const LinuxIcon = () => (
|
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"/>
|
<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>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
const TvIcon = () => (
|
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"/>
|
<rect x="2" y="4" width="20" height="13" rx="2" ry="2"/>
|
||||||
<polyline points="8 21 12 17 16 21"/>
|
<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>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -64,72 +99,37 @@ const platformOrder = ['ios', 'android', 'windows', 'macos', 'linux', 'androidTV
|
|||||||
// Dangerous schemes that should never be allowed
|
// Dangerous schemes that should never be allowed
|
||||||
const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:']
|
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 {
|
function isValidExternalUrl(url: string | undefined): boolean {
|
||||||
if (!url) return false
|
if (!url) return false
|
||||||
const lowerUrl = url.toLowerCase().trim()
|
const lowerUrl = url.toLowerCase().trim()
|
||||||
|
|
||||||
// Block dangerous schemes
|
|
||||||
const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:']
|
|
||||||
if (dangerousSchemes.some(scheme => lowerUrl.startsWith(scheme))) {
|
if (dangerousSchemes.some(scheme => lowerUrl.startsWith(scheme))) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow only http/https URLs
|
|
||||||
return lowerUrl.startsWith('http://') || lowerUrl.startsWith('https://')
|
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 {
|
function isValidDeepLink(url: string | undefined): boolean {
|
||||||
if (!url) return false
|
if (!url) return false
|
||||||
const lowerUrl = url.toLowerCase().trim()
|
const lowerUrl = url.toLowerCase().trim()
|
||||||
|
|
||||||
// Block dangerous schemes
|
|
||||||
if (dangerousSchemes.some(scheme => lowerUrl.startsWith(scheme))) {
|
if (dangerousSchemes.some(scheme => lowerUrl.startsWith(scheme))) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow any URL that has a scheme (contains ://)
|
|
||||||
return lowerUrl.includes('://')
|
return lowerUrl.includes('://')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect user's platform from user agent
|
|
||||||
function detectPlatform(): string | null {
|
function detectPlatform(): string | null {
|
||||||
if (typeof window === 'undefined' || !navigator?.userAgent) {
|
if (typeof window === 'undefined' || !navigator?.userAgent) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const ua = navigator.userAgent.toLowerCase()
|
const ua = navigator.userAgent.toLowerCase()
|
||||||
|
if (/iphone|ipad|ipod/.test(ua)) return 'ios'
|
||||||
// Check for mobile devices first
|
|
||||||
if (/iphone|ipad|ipod/.test(ua)) {
|
|
||||||
return 'ios'
|
|
||||||
}
|
|
||||||
if (/android/.test(ua)) {
|
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'
|
return 'android'
|
||||||
}
|
}
|
||||||
|
if (/macintosh|mac os x/.test(ua)) return 'macos'
|
||||||
// Desktop platforms
|
if (/windows/.test(ua)) return 'windows'
|
||||||
if (/macintosh|mac os x/.test(ua)) {
|
if (/linux/.test(ua)) return 'linux'
|
||||||
return 'macos'
|
|
||||||
}
|
|
||||||
if (/windows/.test(ua)) {
|
|
||||||
return 'windows'
|
|
||||||
}
|
|
||||||
if (/linux/.test(ua)) {
|
|
||||||
return 'linux'
|
|
||||||
}
|
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,20 +145,16 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
|||||||
queryFn: () => subscriptionApi.getAppConfig(),
|
queryFn: () => subscriptionApi.getAppConfig(),
|
||||||
})
|
})
|
||||||
|
|
||||||
// Auto-detect platform on mount
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const detected = detectPlatform()
|
setDetectedPlatform(detectPlatform())
|
||||||
setDetectedPlatform(detected)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Helper to get localized text
|
|
||||||
const getLocalizedText = (text: LocalizedText | undefined): string => {
|
const getLocalizedText = (text: LocalizedText | undefined): string => {
|
||||||
if (!text) return ''
|
if (!text) return ''
|
||||||
const lang = i18n.language || 'en'
|
const lang = i18n.language || 'en'
|
||||||
return text[lang] || text['en'] || text['ru'] || Object.values(text)[0] || ''
|
return text[lang] || text['en'] || text['ru'] || Object.values(text)[0] || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get platform name
|
|
||||||
const getPlatformName = (platformKey: string): string => {
|
const getPlatformName = (platformKey: string): string => {
|
||||||
if (!appConfig?.platformNames?.[platformKey]) {
|
if (!appConfig?.platformNames?.[platformKey]) {
|
||||||
return platformKey
|
return platformKey
|
||||||
@@ -166,13 +162,11 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
|||||||
return getLocalizedText(appConfig.platformNames[platformKey])
|
return getLocalizedText(appConfig.platformNames[platformKey])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get available platforms sorted (detected platform first)
|
|
||||||
const availablePlatforms = useMemo(() => {
|
const availablePlatforms = useMemo(() => {
|
||||||
if (!appConfig?.platforms) return []
|
if (!appConfig?.platforms) return []
|
||||||
const available = platformOrder.filter(
|
const available = platformOrder.filter(
|
||||||
(key) => appConfig.platforms[key] && appConfig.platforms[key].length > 0
|
(key) => appConfig.platforms[key] && appConfig.platforms[key].length > 0
|
||||||
)
|
)
|
||||||
// Move detected platform to the front
|
|
||||||
if (detectedPlatform && available.includes(detectedPlatform)) {
|
if (detectedPlatform && available.includes(detectedPlatform)) {
|
||||||
const filtered = available.filter(p => p !== detectedPlatform)
|
const filtered = available.filter(p => p !== detectedPlatform)
|
||||||
return [detectedPlatform, ...filtered]
|
return [detectedPlatform, ...filtered]
|
||||||
@@ -180,13 +174,11 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
|||||||
return available
|
return available
|
||||||
}, [appConfig, detectedPlatform])
|
}, [appConfig, detectedPlatform])
|
||||||
|
|
||||||
// Get apps for selected platform
|
|
||||||
const platformApps = useMemo(() => {
|
const platformApps = useMemo(() => {
|
||||||
if (!selectedPlatform || !appConfig?.platforms?.[selectedPlatform]) return []
|
if (!selectedPlatform || !appConfig?.platforms?.[selectedPlatform]) return []
|
||||||
return appConfig.platforms[selectedPlatform]
|
return appConfig.platforms[selectedPlatform]
|
||||||
}, [selectedPlatform, appConfig])
|
}, [selectedPlatform, appConfig])
|
||||||
|
|
||||||
// Copy subscription link
|
|
||||||
const copySubscriptionLink = async () => {
|
const copySubscriptionLink = async () => {
|
||||||
if (!appConfig?.subscriptionUrl) return
|
if (!appConfig?.subscriptionUrl) return
|
||||||
try {
|
try {
|
||||||
@@ -194,7 +186,6 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
|||||||
setCopied(true)
|
setCopied(true)
|
||||||
setTimeout(() => setCopied(false), 2000)
|
setTimeout(() => setCopied(false), 2000)
|
||||||
} catch {
|
} catch {
|
||||||
// Fallback for older browsers
|
|
||||||
const textarea = document.createElement('textarea')
|
const textarea = document.createElement('textarea')
|
||||||
textarea.value = appConfig.subscriptionUrl
|
textarea.value = appConfig.subscriptionUrl
|
||||||
document.body.appendChild(textarea)
|
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) => {
|
const handleConnect = (app: AppInfo) => {
|
||||||
// Validate deep link to prevent XSS
|
|
||||||
if (!app.deepLink || !isValidDeepLink(app.deepLink)) {
|
if (!app.deepLink || !isValidDeepLink(app.deepLink)) {
|
||||||
console.warn('Invalid or missing deep link:', app.deepLink)
|
console.warn('Invalid or missing deep link:', app.deepLink)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const lang = i18n.language?.startsWith('ru') ? 'ru' : 'en'
|
const lang = i18n.language?.startsWith('ru') ? 'ru' : 'en'
|
||||||
const redirectUrl = `${window.location.origin}/miniapp/redirect.html?url=${encodeURIComponent(app.deepLink)}&lang=${lang}`
|
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 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) {
|
if (isCustomScheme && tg?.openLink) {
|
||||||
// For custom URL schemes - open redirect page in external browser via Telegram
|
|
||||||
// try_browser: true - показывает диалог для перехода во внешний браузер (важно для мобильных)
|
|
||||||
try {
|
try {
|
||||||
tg.openLink(redirectUrl, { try_instant_view: false, try_browser: true })
|
tg.openLink(redirectUrl, { try_instant_view: false, try_browser: true })
|
||||||
return
|
return
|
||||||
@@ -231,67 +214,55 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
|||||||
console.warn('tg.openLink failed:', e)
|
console.warn('tg.openLink failed:', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback - direct navigation
|
|
||||||
window.location.href = redirectUrl
|
window.location.href = redirectUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modal wrapper classes - bottom sheet on mobile, centered on desktop
|
// Loading state
|
||||||
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]"
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className={modalOverlayClass} onClick={onClose}>
|
<div className="fixed inset-0 bg-black/70 z-50 flex items-end sm:items-center sm:justify-center" onClick={onClose}>
|
||||||
<div className={modalCardClass} onClick={(e) => e.stopPropagation()}>
|
<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={modalContentClass}>
|
<div className="flex justify-center py-12">
|
||||||
<div className="flex justify-center py-8">
|
<div className="w-10 h-10 border-3 border-accent-500 border-t-transparent rounded-full animate-spin" />
|
||||||
<div className="w-8 h-8 border-2 border-accent-500 border-t-transparent rounded-full animate-spin" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error state
|
||||||
if (error || !appConfig) {
|
if (error || !appConfig) {
|
||||||
return (
|
return (
|
||||||
<div className={modalOverlayClass} onClick={onClose}>
|
<div className="fixed inset-0 bg-black/70 z-50 flex items-end sm:items-center sm:justify-center" onClick={onClose}>
|
||||||
<div className={modalCardClass} onClick={(e) => e.stopPropagation()}>
|
<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={modalContentClass}>
|
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<p className="text-error-400">{t('common.error')}</p>
|
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-error-500/10 flex items-center justify-center">
|
||||||
<button onClick={onClose} className="btn-secondary mt-4">
|
<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')}
|
{t('common.close')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No subscription
|
||||||
if (!appConfig.hasSubscription) {
|
if (!appConfig.hasSubscription) {
|
||||||
return (
|
return (
|
||||||
<div className={modalOverlayClass} onClick={onClose}>
|
<div className="fixed inset-0 bg-black/70 z-50 flex items-end sm:items-center sm:justify-center" onClick={onClose}>
|
||||||
<div className={modalCardClass} onClick={(e) => e.stopPropagation()}>
|
<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={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">
|
<div className="text-center py-8">
|
||||||
<p className="text-dark-400">{t('subscription.connection.noSubscription')}</p>
|
<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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -301,75 +272,86 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
|||||||
// Step 1: Select platform
|
// Step 1: Select platform
|
||||||
if (!selectedPlatform) {
|
if (!selectedPlatform) {
|
||||||
return (
|
return (
|
||||||
<div className={modalOverlayClass} onClick={onClose}>
|
<div className="fixed inset-0 bg-black/70 z-50 flex items-end sm:items-center sm:justify-center" onClick={onClose}>
|
||||||
<div className={`${modalCardClass} ${modalScrollableClass} flex flex-col animate-slide-up`} onClick={(e) => e.stopPropagation()}>
|
<div
|
||||||
{/* Header - fixed */}
|
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"
|
||||||
<div className="flex justify-between items-center p-4 sm:p-6 pb-0 flex-shrink-0">
|
onClick={(e) => e.stopPropagation()}
|
||||||
<h2 className="text-lg font-semibold text-dark-100">
|
>
|
||||||
{t('subscription.connection.title')}
|
{/* Header */}
|
||||||
</h2>
|
<div className="flex items-center justify-between p-4 border-b border-dark-800">
|
||||||
<button onClick={onClose} className="btn-icon">
|
<div className="flex items-center gap-3">
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<div className="w-10 h-10 rounded-xl bg-accent-500/10 flex items-center justify-center">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
<span className="text-xl">📱</span>
|
||||||
</svg>
|
</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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scrollable content */}
|
{/* Content */}
|
||||||
<div className="flex-1 min-h-0 overflow-y-auto p-4 sm:p-6 pt-3 sm:pt-4">
|
<div className="flex-1 overflow-y-auto p-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-3">
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2 sm:gap-3">
|
|
||||||
{availablePlatforms.map((platform) => {
|
{availablePlatforms.map((platform) => {
|
||||||
const IconComponent = platformIconComponents[platform]
|
const IconComponent = platformIconComponents[platform]
|
||||||
|
const isDetected = platform === detectedPlatform
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={platform}
|
key={platform}
|
||||||
onClick={() => setSelectedPlatform(platform)}
|
onClick={() => setSelectedPlatform(platform)}
|
||||||
className={`p-3 sm:p-4 rounded-xl border transition-all text-left group ${
|
className={`relative p-4 rounded-2xl border-2 transition-all text-left group ${
|
||||||
platform === detectedPlatform
|
isDetected
|
||||||
? 'bg-accent-500/10 border-accent-500/50 hover:border-accent-500'
|
? '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">
|
{isDetected && (
|
||||||
<div className={`${
|
<div className="absolute -top-2 -right-2 px-2 py-0.5 rounded-full text-[10px] font-medium bg-accent-500 text-white">
|
||||||
platform === detectedPlatform ? 'text-accent-400' : 'text-dark-400 group-hover:text-dark-200'
|
{t('subscription.connection.yourDevice')}
|
||||||
} transition-colors`}>
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`mb-3 ${isDetected ? 'text-accent-400' : 'text-dark-400 group-hover:text-dark-300'} transition-colors`}>
|
||||||
{IconComponent && <IconComponent />}
|
{IconComponent && <IconComponent />}
|
||||||
</div>
|
</div>
|
||||||
{platform === detectedPlatform && (
|
<p className={`font-medium ${isDetected ? 'text-accent-300' : 'text-dark-200'}`}>
|
||||||
<span className="px-2 py-0.5 rounded-full text-xs bg-accent-500/20 text-accent-400">
|
{getPlatformName(platform)}
|
||||||
{t('subscription.connection.yourDevice')}
|
</p>
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-dark-200 font-medium mt-2">{getPlatformName(platform)}</p>
|
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer - fixed with safe area */}
|
{/* Footer */}
|
||||||
<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">
|
<div className="p-4 border-t border-dark-800 pb-[calc(1rem+env(safe-area-inset-bottom,0px))] sm:pb-4">
|
||||||
<button
|
<button
|
||||||
onClick={copySubscriptionLink}
|
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 ? (
|
{copied ? (
|
||||||
<>
|
<>
|
||||||
<svg className="w-5 h-5 text-success-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<div className="w-8 h-8 rounded-full bg-success-500/20 flex items-center justify-center">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
<CheckIcon />
|
||||||
</svg>
|
</div>
|
||||||
<span className="text-success-400">{t('subscription.connection.copied')}</span>
|
<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}>
|
<div className="w-8 h-8 rounded-full bg-dark-700 flex items-center justify-center text-dark-400">
|
||||||
<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" />
|
<CopyIcon />
|
||||||
</svg>
|
</div>
|
||||||
<span className="text-dark-300">{t('subscription.connection.copyLink')}</span>
|
<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>
|
</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) {
|
if (!selectedApp) {
|
||||||
return (
|
return (
|
||||||
<div className={modalOverlayClass} onClick={onClose}>
|
<div className="fixed inset-0 bg-black/70 z-50 flex items-end sm:items-center sm:justify-center" onClick={onClose}>
|
||||||
<div className={`${modalCardClass} ${modalScrollableClass} flex flex-col animate-slide-up`} onClick={(e) => e.stopPropagation()}>
|
<div
|
||||||
{/* Header - fixed */}
|
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"
|
||||||
<div className="flex justify-between items-center p-4 sm:p-6 pb-0 flex-shrink-0">
|
onClick={(e) => e.stopPropagation()}
|
||||||
<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}>
|
{/* Header */}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
<div className="flex items-center justify-between p-4 border-b border-dark-800">
|
||||||
</svg>
|
<div className="flex items-center gap-2">
|
||||||
|
<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>
|
</button>
|
||||||
<h2 className="text-lg font-semibold text-dark-100">
|
<div>
|
||||||
{getPlatformName(selectedPlatform)}
|
<h2 className="font-semibold text-dark-100">{getPlatformName(selectedPlatform)}</h2>
|
||||||
</h2>
|
<p className="text-xs text-dark-500">{t('subscription.connection.selectApp')}</p>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={onClose} className="btn-icon">
|
</div>
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<button onClick={onClose} className="p-2 rounded-xl hover:bg-dark-800 text-dark-400 hover:text-dark-200 transition-colors">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
<CloseIcon />
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scrollable content */}
|
{/* 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">
|
<div className="flex-1 overflow-y-auto p-4 pb-[calc(1rem+env(safe-area-inset-bottom,0px))] sm:pb-4">
|
||||||
<p className="text-dark-400 text-sm sm:text-base mb-3 sm:mb-4">{t('subscription.connection.selectApp')}</p>
|
|
||||||
|
|
||||||
{platformApps.length === 0 ? (
|
{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>
|
<p className="text-dark-500">{t('subscription.connection.noApps')}</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2 sm:space-y-3">
|
<div className="space-y-2">
|
||||||
{platformApps.map((app) => (
|
{platformApps.map((app) => (
|
||||||
<button
|
<button
|
||||||
key={app.id}
|
key={app.id}
|
||||||
onClick={() => setSelectedApp(app)}
|
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-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">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-dark-100 font-medium">{app.name}</span>
|
<span className="font-medium text-dark-100">{app.name}</span>
|
||||||
{app.isFeatured && (
|
{app.isFeatured && (
|
||||||
<span className="px-2 py-0.5 rounded-full text-xs bg-accent-500/20 text-accent-400">
|
<span className="px-1.5 py-0.5 rounded text-[10px] font-medium bg-accent-500/20 text-accent-400">
|
||||||
{t('subscription.connection.featured')}
|
{t('subscription.connection.featured')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<svg className="w-5 h-5 text-dark-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
</div>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
<div className="text-dark-500 group-hover:text-dark-300 transition-colors">
|
||||||
</svg>
|
<ChevronIcon />
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -445,46 +430,44 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 3: Show app instructions and connect button
|
// Step 3: App instructions
|
||||||
return (
|
return (
|
||||||
<div className={modalOverlayClass} onClick={onClose}>
|
<div className="fixed inset-0 bg-black/70 z-50 flex items-end sm:items-center sm:justify-center" onClick={onClose}>
|
||||||
<div className={`${modalCardClass} ${modalScrollableClass} flex flex-col animate-slide-up`} onClick={(e) => e.stopPropagation()}>
|
<div
|
||||||
{/* Header - fixed */}
|
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"
|
||||||
<div className="flex justify-between items-center p-4 sm:p-6 pb-0 flex-shrink-0">
|
onClick={(e) => e.stopPropagation()}
|
||||||
<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}>
|
{/* Header */}
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
<div className="flex items-center justify-between p-4 border-b border-dark-800">
|
||||||
</svg>
|
<div className="flex items-center gap-2">
|
||||||
|
<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>
|
</button>
|
||||||
<h2 className="text-lg font-semibold text-dark-100">
|
<div>
|
||||||
{selectedApp.name}
|
<h2 className="font-semibold text-dark-100">{selectedApp.name}</h2>
|
||||||
</h2>
|
<p className="text-xs text-dark-500">Инструкция по подключению</p>
|
||||||
</div>
|
</div>
|
||||||
<button onClick={onClose} className="btn-icon">
|
</div>
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
<button onClick={onClose} className="p-2 rounded-xl hover:bg-dark-800 text-dark-400 hover:text-dark-200 transition-colors">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
<CloseIcon />
|
||||||
</svg>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scrollable content */}
|
{/* Content */}
|
||||||
<div className="flex-1 min-h-0 overflow-y-auto p-4 sm:p-6 pt-3 sm:pt-4">
|
<div className="flex-1 overflow-y-auto p-4">
|
||||||
<div className="space-y-4 sm:space-y-6">
|
<div className="space-y-4">
|
||||||
{/* Step 1: Install */}
|
{/* Step 1: Install */}
|
||||||
{selectedApp.installationStep && (
|
{selectedApp.installationStep && (
|
||||||
<div className="space-y-2 sm:space-y-3">
|
<div className="p-4 rounded-2xl bg-dark-800/50 border border-dark-700/50">
|
||||||
<h3 className="text-sm font-medium text-accent-400">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
{t('subscription.connection.installApp')}
|
<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>
|
<h3 className="font-medium text-dark-100">{t('subscription.connection.installApp')}</h3>
|
||||||
<p className="text-sm text-dark-400">
|
</div>
|
||||||
|
<p className="text-sm text-dark-400 mb-3 pl-11">
|
||||||
{getLocalizedText(selectedApp.installationStep.description)}
|
{getLocalizedText(selectedApp.installationStep.description)}
|
||||||
</p>
|
</p>
|
||||||
{selectedApp.installationStep.buttons && selectedApp.installationStep.buttons.length > 0 && (
|
{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
|
{selectedApp.installationStep.buttons
|
||||||
.filter((btn) => isValidExternalUrl(btn.buttonLink))
|
.filter((btn) => isValidExternalUrl(btn.buttonLink))
|
||||||
.map((btn, idx) => (
|
.map((btn, idx) => (
|
||||||
@@ -495,9 +478,7 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
|||||||
rel="noopener noreferrer"
|
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"
|
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}>
|
<LinkIcon />
|
||||||
<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>
|
|
||||||
{getLocalizedText(btn.buttonText)}
|
{getLocalizedText(btn.buttonText)}
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
@@ -506,88 +487,53 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
|||||||
</div>
|
</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 */}
|
{/* Step 2: Add subscription */}
|
||||||
{selectedApp.addSubscriptionStep && (
|
{selectedApp.addSubscriptionStep && (
|
||||||
<div className="space-y-2 sm:space-y-3">
|
<div className="p-4 rounded-2xl bg-dark-800/50 border border-dark-700/50">
|
||||||
<h3 className="text-sm font-medium text-accent-400">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
{t('subscription.connection.addSubscription')}
|
<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>
|
<h3 className="font-medium text-dark-100">{t('subscription.connection.addSubscription')}</h3>
|
||||||
<p className="text-sm text-dark-400">
|
</div>
|
||||||
|
<p className="text-sm text-dark-400 mb-4 pl-11">
|
||||||
{getLocalizedText(selectedApp.addSubscriptionStep.description)}
|
{getLocalizedText(selectedApp.addSubscriptionStep.description)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-2 pl-11">
|
||||||
{/* Connect button */}
|
{/* Connect button */}
|
||||||
{selectedApp.deepLink && (
|
{selectedApp.deepLink && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleConnect(selectedApp)}
|
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"
|
className="btn-primary w-full py-3 flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
<LinkIcon />
|
||||||
<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 })}
|
{t('subscription.connection.addToApp', { appName: selectedApp.name })}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Copy link fallback */}
|
{/* Copy link */}
|
||||||
<button
|
<button
|
||||||
onClick={copySubscriptionLink}
|
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-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 ? (
|
{copied ? <CheckIcon /> : <CopyIcon />}
|
||||||
<>
|
{copied ? t('subscription.connection.copied') : t('subscription.connection.copyLink')}
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Step 3: Connect */}
|
{/* Step 3: Connect */}
|
||||||
{selectedApp.connectAndUseStep && (
|
{selectedApp.connectAndUseStep && (
|
||||||
<div className="space-y-2 sm:space-y-3">
|
<div className="p-4 rounded-2xl bg-dark-800/50 border border-dark-700/50">
|
||||||
<h3 className="text-sm font-medium text-accent-400">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
{t('subscription.connection.connectVpn')}
|
<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>
|
<h3 className="font-medium text-dark-100">{t('subscription.connection.connectVpn')}</h3>
|
||||||
<p className="text-sm text-dark-400">
|
</div>
|
||||||
|
<p className="text-sm text-dark-400 pl-11">
|
||||||
{getLocalizedText(selectedApp.connectAndUseStep.description)}
|
{getLocalizedText(selectedApp.connectAndUseStep.description)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -595,9 +541,9 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer - fixed with safe area */}
|
{/* Footer */}
|
||||||
<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">
|
<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 text-sm sm:text-base">
|
<button onClick={onClose} className="btn-secondary w-full">
|
||||||
{t('common.close')}
|
{t('common.close')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user