import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useTranslation } from 'react-i18next' import { adminAppsApi, AppDefinition, LocalizedText, AppStep, AppButton } from '../api/adminApps' // Icons const AppsIcon = () => ( ) const PlusIcon = () => ( ) const EditIcon = () => ( ) const TrashIcon = () => ( ) const ChevronUpIcon = () => ( ) const ChevronDownIcon = () => ( ) const CopyIcon = () => ( ) const StarIcon = ({ filled }: { filled: boolean }) => ( ) const PLATFORM_LABELS: Record = { ios: 'iOS', android: 'Android', macos: 'macOS', windows: 'Windows', linux: 'Linux', androidTV: 'Android TV', appleTV: 'Apple TV', } const PLATFORMS = ['ios', 'android', 'macos', 'windows', 'linux', 'androidTV', 'appleTV'] // Helper to create empty localized text const emptyLocalizedText = (): LocalizedText => ({ en: '', ru: '', zh: '', fa: '', }) // Helper to create empty app step const emptyAppStep = (): AppStep => ({ description: emptyLocalizedText(), buttons: [], }) // Helper to create empty app const createEmptyApp = (platform: string): AppDefinition => ({ id: `new-app-${platform}-${Date.now()}`, name: '', isFeatured: false, urlScheme: '', installationStep: emptyAppStep(), addSubscriptionStep: emptyAppStep(), connectAndUseStep: emptyAppStep(), }) interface AppEditorModalProps { app: AppDefinition platform: string isNew: boolean onSave: (app: AppDefinition) => void onClose: () => void } function AppEditorModal({ app, platform, isNew, onSave, onClose }: AppEditorModalProps) { const { t } = useTranslation() const [editedApp, setEditedApp] = useState({ ...app }) const [activeTab, setActiveTab] = useState<'basic' | 'installation' | 'subscription' | 'connect' | 'additional'>('basic') const updateField = (field: K, value: AppDefinition[K]) => { setEditedApp((prev) => ({ ...prev, [field]: value })) } const updateLocalizedText = ( stepKey: 'installationStep' | 'addSubscriptionStep' | 'connectAndUseStep' | 'additionalBeforeAddSubscriptionStep' | 'additionalAfterAddSubscriptionStep', field: 'description' | 'title', lang: keyof LocalizedText, value: string ) => { setEditedApp((prev) => { const step = prev[stepKey] || emptyAppStep() const fieldValue = step[field] || emptyLocalizedText() return { ...prev, [stepKey]: { ...step, [field]: { ...fieldValue, [lang]: value }, }, } }) } const updateButtons = ( stepKey: 'installationStep' | 'addSubscriptionStep' | 'connectAndUseStep' | 'additionalBeforeAddSubscriptionStep' | 'additionalAfterAddSubscriptionStep', buttons: AppButton[] ) => { setEditedApp((prev) => { const step = prev[stepKey] || emptyAppStep() return { ...prev, [stepKey]: { ...step, buttons }, } }) } const addButton = (stepKey: 'installationStep' | 'additionalBeforeAddSubscriptionStep' | 'additionalAfterAddSubscriptionStep') => { const step = editedApp[stepKey] || emptyAppStep() const buttons = step.buttons || [] updateButtons(stepKey, [...buttons, { buttonLink: '', buttonText: emptyLocalizedText() }]) } const removeButton = (stepKey: 'installationStep' | 'additionalBeforeAddSubscriptionStep' | 'additionalAfterAddSubscriptionStep', index: number) => { const step = editedApp[stepKey] || emptyAppStep() const buttons = step.buttons || [] updateButtons(stepKey, buttons.filter((_, i) => i !== index)) } const updateButton = ( stepKey: 'installationStep' | 'additionalBeforeAddSubscriptionStep' | 'additionalAfterAddSubscriptionStep', index: number, field: 'buttonLink' | 'buttonText', value: string | LocalizedText ) => { const step = editedApp[stepKey] || emptyAppStep() const buttons = [...(step.buttons || [])] buttons[index] = { ...buttons[index], [field]: value } updateButtons(stepKey, buttons) } const renderLocalizedTextInputs = ( stepKey: 'installationStep' | 'addSubscriptionStep' | 'connectAndUseStep' | 'additionalBeforeAddSubscriptionStep' | 'additionalAfterAddSubscriptionStep', field: 'description' | 'title', label: string ) => { const step = editedApp[stepKey] const value = step?.[field] || emptyLocalizedText() return ( {label} {(['en', 'ru'] as const).map((lang) => ( {lang} updateLocalizedText(stepKey, field, lang, e.target.value)} className="input flex-1 text-sm" rows={2} /> ))} ) } const renderButtonsEditor = (stepKey: 'installationStep' | 'additionalBeforeAddSubscriptionStep' | 'additionalAfterAddSubscriptionStep') => { const step = editedApp[stepKey] const buttons = step?.buttons || [] return ( {t('admin.apps.buttons')} addButton(stepKey)} className="text-xs text-accent-400 hover:text-accent-300" > + {t('admin.apps.addButton')} {buttons.map((button, index) => ( {t('admin.apps.button')} #{index + 1} removeButton(stepKey, index)} className="text-error-400 hover:text-error-300" > updateButton(stepKey, index, 'buttonLink', e.target.value)} placeholder="https://..." className="input w-full text-sm" /> {(['en', 'ru'] as const).map((lang) => ( {lang} { const newButtonText = { ...button.buttonText, [lang]: e.target.value } updateButton(stepKey, index, 'buttonText', newButtonText) }} placeholder={t('admin.apps.buttonText')} className="input flex-1 text-sm" /> ))} ))} ) } return ( {/* Header */} {isNew ? t('admin.apps.createApp') : t('admin.apps.editApp')} - {PLATFORM_LABELS[platform]} {/* Tabs */} {(['basic', 'installation', 'subscription', 'connect', 'additional'] as const).map((tab) => ( setActiveTab(tab)} className={`px-4 py-2 text-sm font-medium whitespace-nowrap ${ activeTab === tab ? 'text-accent-400 border-b-2 border-accent-400' : 'text-dark-400 hover:text-dark-200' }`} > {t(`admin.apps.tabs.${tab}`)} ))} {/* Content */} {activeTab === 'basic' && ( <> {t('admin.apps.appId')} updateField('id', e.target.value)} className="input w-full" disabled={!isNew} /> {t('admin.apps.appName')} updateField('name', e.target.value)} className="input w-full" /> {t('admin.apps.urlScheme')} updateField('urlScheme', e.target.value)} className="input w-full" placeholder="app://add/" /> updateField('isFeatured', e.target.checked)} className="w-4 h-4 accent-accent-500" /> {t('admin.apps.featured')} updateField('isNeedBase64Encoding', e.target.checked || undefined)} className="w-4 h-4 accent-accent-500" /> {t('admin.apps.base64Encoding')} > )} {activeTab === 'installation' && ( <> {renderLocalizedTextInputs('installationStep', 'description', t('admin.apps.installDescription'))} {renderButtonsEditor('installationStep')} > )} {activeTab === 'subscription' && ( <> {renderLocalizedTextInputs('addSubscriptionStep', 'description', t('admin.apps.subscriptionDescription'))} > )} {activeTab === 'connect' && ( <> {renderLocalizedTextInputs('connectAndUseStep', 'description', t('admin.apps.connectDescription'))} > )} {activeTab === 'additional' && ( <> {t('admin.apps.beforeSubscription')} {renderLocalizedTextInputs('additionalBeforeAddSubscriptionStep', 'title', t('admin.apps.stepTitle'))} {renderLocalizedTextInputs('additionalBeforeAddSubscriptionStep', 'description', t('admin.apps.stepDescription'))} {renderButtonsEditor('additionalBeforeAddSubscriptionStep')} {t('admin.apps.afterSubscription')} {renderLocalizedTextInputs('additionalAfterAddSubscriptionStep', 'title', t('admin.apps.stepTitle'))} {renderLocalizedTextInputs('additionalAfterAddSubscriptionStep', 'description', t('admin.apps.stepDescription'))} {renderButtonsEditor('additionalAfterAddSubscriptionStep')} > )} {/* Footer */} {t('common.cancel')} onSave(editedApp)} className="btn-primary"> {t('common.save')} ) } export default function AdminApps() { const { t } = useTranslation() const queryClient = useQueryClient() const [selectedPlatform, setSelectedPlatform] = useState('ios') const [editingApp, setEditingApp] = useState<{ app: AppDefinition; isNew: boolean } | null>(null) const [copyTarget, setCopyTarget] = useState<{ appId: string; platform: string } | null>(null) const { data: apps, isLoading } = useQuery({ queryKey: ['admin-apps', selectedPlatform], queryFn: () => adminAppsApi.getPlatformApps(selectedPlatform), }) const createMutation = useMutation({ mutationFn: ({ platform, app }: { platform: string; app: AppDefinition }) => adminAppsApi.createApp(platform, app), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-apps'] }) setEditingApp(null) }, }) const updateMutation = useMutation({ mutationFn: ({ platform, appId, app }: { platform: string; appId: string; app: AppDefinition }) => adminAppsApi.updateApp(platform, appId, app), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-apps'] }) setEditingApp(null) }, }) const deleteMutation = useMutation({ mutationFn: ({ platform, appId }: { platform: string; appId: string }) => adminAppsApi.deleteApp(platform, appId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-apps'] }) }, }) const reorderMutation = useMutation({ mutationFn: ({ platform, appIds }: { platform: string; appIds: string[] }) => adminAppsApi.reorderApps(platform, appIds), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-apps'] }) }, }) const copyMutation = useMutation({ mutationFn: ({ platform, appId, targetPlatform }: { platform: string; appId: string; targetPlatform: string }) => adminAppsApi.copyApp(platform, appId, targetPlatform), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-apps'] }) setCopyTarget(null) }, }) const handleMoveUp = (index: number) => { if (!apps || index === 0) return const newOrder = [...apps] ;[newOrder[index - 1], newOrder[index]] = [newOrder[index], newOrder[index - 1]] reorderMutation.mutate({ platform: selectedPlatform, appIds: newOrder.map((a) => a.id) }) } const handleMoveDown = (index: number) => { if (!apps || index === apps.length - 1) return const newOrder = [...apps] ;[newOrder[index], newOrder[index + 1]] = [newOrder[index + 1], newOrder[index]] reorderMutation.mutate({ platform: selectedPlatform, appIds: newOrder.map((a) => a.id) }) } const handleSave = (app: AppDefinition) => { if (editingApp?.isNew) { createMutation.mutate({ platform: selectedPlatform, app }) } else { updateMutation.mutate({ platform: selectedPlatform, appId: app.id, app }) } } const handleDelete = (appId: string) => { if (confirm(t('admin.apps.confirmDelete'))) { deleteMutation.mutate({ platform: selectedPlatform, appId }) } } return ( {t('admin.apps.title')} {/* Platform Tabs */} {PLATFORMS.map((platform) => ( setSelectedPlatform(platform)} className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${ selectedPlatform === platform ? 'bg-accent-500 text-white' : 'bg-dark-800 text-dark-300 hover:bg-dark-700' }`} > {PLATFORM_LABELS[platform]} ))} {/* Add App Button */} setEditingApp({ app: createEmptyApp(selectedPlatform), isNew: true })} className="btn-primary flex items-center gap-2" > {t('admin.apps.addApp')} {/* Apps List */} {isLoading ? ( ) : apps && apps.length > 0 ? ( {apps.map((app, index) => ( {/* Reorder buttons */} handleMoveUp(index)} disabled={index === 0} className="p-1 text-dark-400 hover:text-dark-200 disabled:opacity-30 disabled:cursor-not-allowed" > handleMoveDown(index)} disabled={index === apps.length - 1} className="p-1 text-dark-400 hover:text-dark-200 disabled:opacity-30 disabled:cursor-not-allowed" > {/* App Info */} {app.name} {app.isFeatured && } {app.isNeedBase64Encoding && ( Base64 )} {app.id} {app.urlScheme} {/* Actions */} setCopyTarget({ appId: app.id, platform: selectedPlatform })} className="p-2 text-dark-400 hover:text-dark-200 hover:bg-dark-700 rounded" title={t('admin.apps.copyTo')} > setEditingApp({ app, isNew: false })} className="p-2 text-dark-400 hover:text-dark-200 hover:bg-dark-700 rounded" title={t('common.edit')} > handleDelete(app.id)} className="p-2 text-error-400 hover:text-error-300 hover:bg-error-500/10 rounded" title={t('common.delete')} > ))} ) : ( {t('admin.apps.noApps')} setEditingApp({ app: createEmptyApp(selectedPlatform), isNew: true })} className="btn-primary mt-4" > {t('admin.apps.addFirstApp')} )} {/* Edit Modal */} {editingApp && ( setEditingApp(null)} /> )} {/* Copy Modal */} {copyTarget && ( {t('admin.apps.copyTo')} {PLATFORMS.filter((p) => p !== copyTarget.platform).map((platform) => ( copyMutation.mutate({ platform: copyTarget.platform, appId: copyTarget.appId, targetPlatform: platform, })} className="px-4 py-2 bg-dark-800 hover:bg-dark-700 rounded-lg text-dark-200 text-sm" > {PLATFORM_LABELS[platform]} ))} setCopyTarget(null)} className="btn-secondary w-full mt-4" > {t('common.cancel')} )} ) }
{t('admin.apps.noApps')}