Refactor ConnectionModal: implement auto-selection of platform and app upon data load, enhance app selection UI with improved layout and functionality, and add translation keys for 'changeApp' in English and Russian locales.

This commit is contained in:
PEDZEO
2026-01-20 03:16:33 +03:00
parent 026ba65b8c
commit 4c489547ea
3 changed files with 101 additions and 117 deletions

View File

@@ -201,10 +201,10 @@ function detectPlatform(): string | null {
export default function ConnectionModal({ onClose }: ConnectionModalProps) { export default function ConnectionModal({ onClose }: ConnectionModalProps) {
const { t, i18n } = useTranslation() const { t, i18n } = useTranslation()
const [selectedPlatform, setSelectedPlatform] = useState<string | null>(null)
const [selectedApp, setSelectedApp] = useState<AppInfo | null>(null) const [selectedApp, setSelectedApp] = useState<AppInfo | null>(null)
const [copied, setCopied] = useState(false) const [copied, setCopied] = useState(false)
const [detectedPlatform, setDetectedPlatform] = useState<string | null>(null) const [detectedPlatform, setDetectedPlatform] = useState<string | null>(null)
const [showAppSelector, setShowAppSelector] = useState(false)
const { data: appConfig, isLoading, error } = useQuery<AppConfig>({ const { data: appConfig, isLoading, error } = useQuery<AppConfig>({
queryKey: ['appConfig'], queryKey: ['appConfig'],
@@ -215,6 +215,22 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
setDetectedPlatform(detectPlatform()) setDetectedPlatform(detectPlatform())
}, []) }, [])
// Auto-select platform and app when data is loaded
useEffect(() => {
if (!appConfig?.platforms || selectedApp) return
const platform = detectedPlatform || platformOrder.find(p => appConfig.platforms[p]?.length > 0)
if (!platform || !appConfig.platforms[platform]?.length) return
const apps = appConfig.platforms[platform]
// Prefer featured app, otherwise first app
const app = apps.find(a => a.isFeatured) || apps[0]
if (app) {
setSelectedApp(app)
}
}, [appConfig, detectedPlatform, selectedApp])
// Lock body scroll when modal is open (works on iOS too) // Lock body scroll when modal is open (works on iOS too)
useEffect(() => { useEffect(() => {
const scrollY = window.scrollY const scrollY = window.scrollY
@@ -273,11 +289,6 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
return available return available
}, [appConfig, detectedPlatform]) }, [appConfig, detectedPlatform])
const platformApps = useMemo(() => {
if (!selectedPlatform || !appConfig?.platforms?.[selectedPlatform]) return []
return appConfig.platforms[selectedPlatform]
}, [selectedPlatform, appConfig])
const copySubscriptionLink = async () => { const copySubscriptionLink = async () => {
if (!appConfig?.subscriptionUrl) return if (!appConfig?.subscriptionUrl) return
try { try {
@@ -384,18 +395,33 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
) )
} }
// Step 1: Select platform // Get all apps for selector
if (!selectedPlatform) { const allAppsForSelector = useMemo(() => {
if (!appConfig?.platforms) return []
const result: { platform: string; apps: AppInfo[] }[] = []
for (const platform of availablePlatforms) {
const apps = appConfig.platforms[platform]
if (apps?.length) {
result.push({ platform, apps })
}
}
return result
}, [appConfig, availablePlatforms])
// App selector view
if (showAppSelector || !selectedApp) {
return ( return (
<ModalWrapper> <ModalWrapper>
{/* Header */} {/* Header */}
<div className="flex items-center justify-between p-4 border-b border-dark-800"> <div className="flex items-center justify-between p-4 border-b border-dark-800">
<div className="flex items-center gap-3"> <div className="flex items-center gap-2">
<div className="w-9 h-9 rounded-xl bg-accent-500/10 flex items-center justify-center"> {selectedApp && (
<span className="text-lg">📱</span> <button onClick={() => setShowAppSelector(false)} className="p-2 -ml-2 rounded-xl hover:bg-dark-800 text-dark-400">
</div> <BackIcon />
</button>
)}
<div> <div>
<h2 className="font-semibold text-dark-100 text-sm">{t('subscription.connection.title')}</h2> <h2 className="font-semibold text-dark-100 text-sm">{t('subscription.connection.selectApp')}</h2>
<p className="text-xs text-dark-500">{t('subscription.connection.selectDevice')}</p> <p className="text-xs text-dark-500">{t('subscription.connection.selectDevice')}</p>
</div> </div>
</div> </div>
@@ -404,41 +430,58 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
</button> </button>
</div> </div>
{/* Platforms grid */} {/* Apps by platform */}
<div className="p-4"> <div className="p-4 space-y-4 max-h-[60vh] overflow-y-auto">
<div className="grid grid-cols-3 gap-2"> {allAppsForSelector.map(({ platform, apps }) => {
{availablePlatforms.map((platform) => { const IconComponent = platformIconComponents[platform]
const IconComponent = platformIconComponents[platform] const isCurrentPlatform = platform === detectedPlatform
const isDetected = platform === detectedPlatform return (
return ( <div key={platform}>
<button <div className="flex items-center gap-2 mb-2">
key={platform} <div className={`${isCurrentPlatform ? 'text-accent-400' : 'text-dark-500'}`}>
onClick={() => setSelectedPlatform(platform)}
className={`relative p-3 rounded-xl border transition-all flex flex-col items-center gap-2 ${
isDetected
? 'bg-accent-500/10 border-accent-500/50'
: 'bg-dark-800/50 border-dark-700/50 hover:border-dark-600'
}`}
>
{isDetected && (
<div className="absolute -top-1.5 left-1/2 -translate-x-1/2 px-1.5 py-0.5 rounded text-[8px] font-medium bg-accent-500 text-white whitespace-nowrap">
{t('subscription.connection.yourDevice')}
</div>
)}
<div className={isDetected ? 'text-accent-400' : 'text-dark-400'}>
{IconComponent && <IconComponent />} {IconComponent && <IconComponent />}
</div> </div>
<span className={`text-xs font-medium ${isDetected ? 'text-accent-300' : 'text-dark-300'}`}> <span className={`text-xs font-medium ${isCurrentPlatform ? 'text-accent-400' : 'text-dark-400'}`}>
{getPlatformName(platform)} {getPlatformName(platform)}
{isCurrentPlatform && <span className="ml-1 text-[10px]">({t('subscription.connection.yourDevice')})</span>}
</span> </span>
</button> </div>
) <div className="space-y-1.5">
})} {apps.map((app) => (
</div> <button
key={app.id}
onClick={() => {
setSelectedApp(app)
setShowAppSelector(false)
}}
className={`w-full p-2.5 rounded-xl border transition-all flex items-center gap-3 ${
selectedApp?.id === app.id
? 'bg-accent-500/10 border-accent-500/50'
: 'bg-dark-800/50 border-dark-700/50 hover:border-dark-600'
}`}
>
<div className="w-8 h-8 rounded-lg bg-dark-700 flex items-center justify-center text-sm">
{getAppIcon(app.name, app.isFeatured)}
</div>
<div className="flex items-center gap-2 flex-1 min-w-0">
<span className="font-medium text-dark-100 text-sm truncate">{app.name}</span>
{app.isFeatured && (
<span className="px-1.5 py-0.5 rounded text-[9px] font-medium bg-accent-500/20 text-accent-400 shrink-0">
{t('subscription.connection.featured')}
</span>
)}
</div>
<ChevronIcon />
</button>
))}
</div>
</div>
)
})}
</div> </div>
{/* Copy link */} {/* Copy link */}
<div className="px-4 pb-4"> <div className="p-4 border-t border-dark-800">
<button <button
onClick={copySubscriptionLink} onClick={copySubscriptionLink}
className={`w-full p-2.5 rounded-xl border-2 border-dashed transition-all flex items-center justify-center gap-2 text-sm ${ className={`w-full p-2.5 rounded-xl border-2 border-dashed transition-all flex items-center justify-center gap-2 text-sm ${
@@ -455,84 +498,23 @@ export default function ConnectionModal({ onClose }: ConnectionModalProps) {
) )
} }
// Step 2: Select app // App instructions (main view)
if (!selectedApp) {
return (
<ModalWrapper>
{/* 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="p-2 -ml-2 rounded-xl hover:bg-dark-800 text-dark-400">
<BackIcon />
</button>
<div>
<h2 className="font-semibold text-dark-100 text-sm">{getPlatformName(selectedPlatform)}</h2>
<p className="text-xs text-dark-500">{t('subscription.connection.selectApp')}</p>
</div>
</div>
<button onClick={onClose} className="p-2 rounded-xl hover:bg-dark-800 text-dark-400">
<CloseIcon />
</button>
</div>
{/* Apps list */}
<div className="p-4 max-h-[50vh] overflow-y-auto">
{platformApps.length === 0 ? (
<div className="text-center py-8">
<div className="w-12 h-12 mx-auto mb-3 rounded-xl bg-dark-800 flex items-center justify-center">
<span className="text-xl">📭</span>
</div>
<p className="text-dark-500 text-sm">{t('subscription.connection.noApps')}</p>
</div>
) : (
<div className="space-y-2">
{platformApps.map((app) => (
<button
key={app.id}
onClick={() => setSelectedApp(app)}
className="w-full p-3 rounded-xl bg-dark-800/50 border border-dark-700/50 hover:border-dark-600 transition-all flex items-center justify-between"
>
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-lg bg-dark-700 flex items-center justify-center text-base">
{getAppIcon(app.name, app.isFeatured)}
</div>
<div className="text-left">
<div className="flex items-center gap-2">
<span className="font-medium text-dark-100 text-sm">{app.name}</span>
{app.isFeatured && (
<span className="px-1.5 py-0.5 rounded text-[9px] font-medium bg-accent-500/20 text-accent-400">
{t('subscription.connection.featured')}
</span>
)}
</div>
</div>
</div>
<div className="text-dark-500">
<ChevronIcon />
</div>
</button>
))}
</div>
)}
</div>
</ModalWrapper>
)
}
// Step 3: App instructions
return ( return (
<ModalWrapper> <ModalWrapper>
{/* Header */} {/* Header with app info and change button */}
<div className="flex items-center justify-between p-4 border-b border-dark-800"> <div className="flex items-center justify-between p-4 border-b border-dark-800">
<div className="flex items-center gap-2"> <button
<button onClick={() => setSelectedApp(null)} className="p-2 -ml-2 rounded-xl hover:bg-dark-800 text-dark-400"> onClick={() => setShowAppSelector(true)}
<BackIcon /> className="flex items-center gap-3 p-2 -m-2 rounded-xl hover:bg-dark-800/50 transition-colors"
</button> >
<div> <div className="w-10 h-10 rounded-xl bg-dark-700 flex items-center justify-center">
<h2 className="font-semibold text-dark-100 text-sm">{selectedApp.name}</h2> {getAppIcon(selectedApp.name, selectedApp.isFeatured)}
<p className="text-xs text-dark-500">{t('subscription.connection.instructions')}</p>
</div> </div>
</div> <div className="text-left">
<h2 className="font-semibold text-dark-100 text-sm">{selectedApp.name}</h2>
<p className="text-xs text-accent-400">{t('subscription.connection.changeApp') || 'Сменить'} </p>
</div>
</button>
<button onClick={onClose} className="p-2 rounded-xl hover:bg-dark-800 text-dark-400"> <button onClick={onClose} className="p-2 rounded-xl hover:bg-dark-800 text-dark-400">
<CloseIcon /> <CloseIcon />
</button> </button>

View File

@@ -185,7 +185,8 @@
"copyLink": "Copy subscription link", "copyLink": "Copy subscription link",
"copied": "Link copied!", "copied": "Link copied!",
"openLink": "Open link", "openLink": "Open link",
"instructions": "Instructions" "instructions": "Instructions",
"changeApp": "Change app"
}, },
"myDevices": "My Devices", "myDevices": "My Devices",
"noDevices": "No connected devices", "noDevices": "No connected devices",

View File

@@ -185,7 +185,8 @@
"copyLink": "Скопировать ссылку", "copyLink": "Скопировать ссылку",
"copied": "Ссылка скопирована!", "copied": "Ссылка скопирована!",
"openLink": "Открыть ссылку", "openLink": "Открыть ссылку",
"instructions": "Инструкция" "instructions": "Инструкция",
"changeApp": "Сменить приложение"
}, },
"myDevices": "Мои устройства", "myDevices": "Мои устройства",
"noDevices": "Нет подключенных устройств", "noDevices": "Нет подключенных устройств",