mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 17:43:47 +00:00
feat: connect RemnaWave baseTranslations and fix SVG icons
- Wire baseTranslations from RemnaWave config to connection page UI (copyLink, openApp, installApp, addSubscription, connectAndUse, tutorial, close) with locale fallback: current lang → en → ru → first - Add tutorial button when baseSettings.isShowTutorialButton is enabled - Fix SVG icon rendering on admin apps settings page with proper scaling - Add colored circle backgrounds for block and app icons in admin panel - Add tutorial translation key to all locale files
This commit is contained in:
@@ -346,7 +346,8 @@
|
|||||||
"changeApp": "Change app",
|
"changeApp": "Change app",
|
||||||
"selectPlatform": "Select platform",
|
"selectPlatform": "Select platform",
|
||||||
"app": "app",
|
"app": "app",
|
||||||
"apps": "apps"
|
"apps": "apps",
|
||||||
|
"tutorial": "Tutorial"
|
||||||
},
|
},
|
||||||
"myDevices": "My Devices",
|
"myDevices": "My Devices",
|
||||||
"noDevices": "No connected devices",
|
"noDevices": "No connected devices",
|
||||||
|
|||||||
@@ -298,7 +298,8 @@
|
|||||||
"changeApp": "تغییر برنامه",
|
"changeApp": "تغییر برنامه",
|
||||||
"selectPlatform": "انتخاب پلتفرم",
|
"selectPlatform": "انتخاب پلتفرم",
|
||||||
"app": "برنامه",
|
"app": "برنامه",
|
||||||
"apps": "برنامه"
|
"apps": "برنامه",
|
||||||
|
"tutorial": "آموزش"
|
||||||
},
|
},
|
||||||
"additionalOptions": {
|
"additionalOptions": {
|
||||||
"title": "گزینههای اضافی",
|
"title": "گزینههای اضافی",
|
||||||
|
|||||||
@@ -368,7 +368,8 @@
|
|||||||
"changeApp": "Сменить приложение",
|
"changeApp": "Сменить приложение",
|
||||||
"selectPlatform": "Выберите платформу",
|
"selectPlatform": "Выберите платформу",
|
||||||
"app": "приложение",
|
"app": "приложение",
|
||||||
"apps": "приложений"
|
"apps": "приложений",
|
||||||
|
"tutorial": "Инструкция"
|
||||||
},
|
},
|
||||||
"myDevices": "Мои устройства",
|
"myDevices": "Мои устройства",
|
||||||
"noDevices": "Нет подключенных устройств",
|
"noDevices": "Нет подключенных устройств",
|
||||||
|
|||||||
@@ -298,7 +298,8 @@
|
|||||||
"changeApp": "更换应用",
|
"changeApp": "更换应用",
|
||||||
"selectPlatform": "选择平台",
|
"selectPlatform": "选择平台",
|
||||||
"app": "个应用",
|
"app": "个应用",
|
||||||
"apps": "个应用"
|
"apps": "个应用",
|
||||||
|
"tutorial": "教程"
|
||||||
},
|
},
|
||||||
"additionalOptions": {
|
"additionalOptions": {
|
||||||
"title": "附加选项",
|
"title": "附加选项",
|
||||||
|
|||||||
@@ -116,13 +116,13 @@ function getLocalizedText(text: LocalizedText | undefined, lang: string): string
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSvgIcon(svgLibrary: Record<string, RemnawaveSvgItem>, key?: string, color?: string) {
|
function renderSvgIcon(svgLibrary: Record<string, RemnawaveSvgItem>, key?: string, color?: string) {
|
||||||
if (!key || !svgLibrary[key]) return null;
|
if (!key || !svgLibrary[key]?.svgString) return null;
|
||||||
const sanitized = DOMPurify.sanitize(svgLibrary[key].svgString, {
|
const sanitized = DOMPurify.sanitize(svgLibrary[key].svgString, {
|
||||||
USE_PROFILES: { svg: true, svgFilters: true },
|
USE_PROFILES: { svg: true, svgFilters: true },
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="h-6 w-6 flex-shrink-0"
|
className="h-6 w-6 flex-shrink-0 [&>svg]:h-full [&>svg]:w-full"
|
||||||
style={{ color: color || 'currentColor' }}
|
style={{ color: color || 'currentColor' }}
|
||||||
dangerouslySetInnerHTML={{ __html: sanitized }}
|
dangerouslySetInnerHTML={{ __html: sanitized }}
|
||||||
/>
|
/>
|
||||||
@@ -230,7 +230,17 @@ function AppCard({
|
|||||||
className="flex w-full items-center gap-4 p-4 text-left"
|
className="flex w-full items-center gap-4 p-4 text-left"
|
||||||
>
|
>
|
||||||
{/* Icon from first block */}
|
{/* Icon from first block */}
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-dark-700/80 text-dark-300">
|
<div
|
||||||
|
className="flex h-10 w-10 items-center justify-center rounded-full"
|
||||||
|
style={
|
||||||
|
firstBlock?.svgIconColor
|
||||||
|
? {
|
||||||
|
backgroundColor: firstBlock.svgIconColor + '20',
|
||||||
|
color: firstBlock.svgIconColor,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
{renderSvgIcon(svgLibrary, firstBlock?.svgIconKey, firstBlock?.svgIconColor) || (
|
{renderSvgIcon(svgLibrary, firstBlock?.svgIconKey, firstBlock?.svgIconColor) || (
|
||||||
<AppsIcon />
|
<AppsIcon />
|
||||||
)}
|
)}
|
||||||
@@ -287,10 +297,18 @@ function BlockCard({
|
|||||||
const title = getLocalizedText(block.title, lang);
|
const title = getLocalizedText(block.title, lang);
|
||||||
const description = getLocalizedText(block.description, lang);
|
const description = getLocalizedText(block.description, lang);
|
||||||
|
|
||||||
|
const iconColor = block.svgIconColor || '#8B5CF6';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg bg-dark-800/50 p-3">
|
<div className="rounded-lg bg-dark-800/50 p-3">
|
||||||
<div className="flex items-start gap-3">
|
<div className="flex items-start gap-3">
|
||||||
<div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-lg bg-dark-700">
|
<div
|
||||||
|
className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: iconColor + '20',
|
||||||
|
color: iconColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{renderSvgIcon(svgLibrary, block.svgIconKey, block.svgIconColor) || (
|
{renderSvgIcon(svgLibrary, block.svgIconKey, block.svgIconColor) || (
|
||||||
<span className="text-xs font-bold text-dark-400">{index + 1}</span>
|
<span className="text-xs font-bold text-dark-400">{index + 1}</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -230,12 +230,16 @@ export default function Connection() {
|
|||||||
}, [navigate]);
|
}, [navigate]);
|
||||||
|
|
||||||
const handleBack = useCallback(() => {
|
const handleBack = useCallback(() => {
|
||||||
if (selectedPlatform) {
|
if (showAppSelector) {
|
||||||
setSelectedPlatform(null);
|
if (selectedPlatform) {
|
||||||
|
setSelectedPlatform(null);
|
||||||
|
} else {
|
||||||
|
setShowAppSelector(false);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
setShowAppSelector(false);
|
navigate(-1);
|
||||||
}
|
}
|
||||||
}, [selectedPlatform]);
|
}, [showAppSelector, selectedPlatform, navigate]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
@@ -268,6 +272,20 @@ export default function Connection() {
|
|||||||
return text[lang] || text['en'] || text['ru'] || Object.values(text)[0] || '';
|
return text[lang] || text['en'] || text['ru'] || Object.values(text)[0] || '';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get translation from RemnaWave baseTranslations with i18n fallback
|
||||||
|
const getBaseTranslation = (
|
||||||
|
key: string,
|
||||||
|
i18nKey: string,
|
||||||
|
interpolation?: Record<string, string>,
|
||||||
|
): string => {
|
||||||
|
const bt = appConfig?.baseTranslations;
|
||||||
|
if (bt && key in bt) {
|
||||||
|
const text = getLocalizedText(bt[key as keyof typeof bt] as LocalizedText);
|
||||||
|
if (text) return text;
|
||||||
|
}
|
||||||
|
return t(i18nKey, interpolation);
|
||||||
|
};
|
||||||
|
|
||||||
const availablePlatforms = useMemo(() => {
|
const availablePlatforms = useMemo(() => {
|
||||||
if (!appConfig?.platforms) return [];
|
if (!appConfig?.platforms) return [];
|
||||||
const available = platformOrder.filter(
|
const available = platformOrder.filter(
|
||||||
@@ -339,11 +357,6 @@ export default function Connection() {
|
|||||||
openDeepLink(app.deepLink);
|
openDeepLink(app.deepLink);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConnectRemnawave = (app: RemnawaveAppClient) => {
|
|
||||||
if (!app.deepLink || !isValidDeepLink(app.deepLink)) return;
|
|
||||||
openDeepLink(app.deepLink);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Resolve button links for step buttons
|
// Resolve button links for step buttons
|
||||||
const resolveButtonLink = (link: string): string => {
|
const resolveButtonLink = (link: string): string => {
|
||||||
return resolveUrl(link);
|
return resolveUrl(link);
|
||||||
@@ -372,7 +385,7 @@ export default function Connection() {
|
|||||||
<div className="flex flex-1 flex-col items-center justify-center p-8 text-center">
|
<div className="flex flex-1 flex-col items-center justify-center p-8 text-center">
|
||||||
<p className="mb-4 text-lg text-dark-300">{t('common.error')}</p>
|
<p className="mb-4 text-lg text-dark-300">{t('common.error')}</p>
|
||||||
<button onClick={handleGoBack} className="btn-primary px-6 py-2">
|
<button onClick={handleGoBack} className="btn-primary px-6 py-2">
|
||||||
{t('common.close')}
|
{getBaseTranslation('close', 'common.close')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -387,7 +400,7 @@ export default function Connection() {
|
|||||||
</h3>
|
</h3>
|
||||||
<p className="mb-4 text-dark-400">{t('subscription.connection.noSubscription')}</p>
|
<p className="mb-4 text-dark-400">{t('subscription.connection.noSubscription')}</p>
|
||||||
<button onClick={handleGoBack} className="btn-primary px-6 py-2">
|
<button onClick={handleGoBack} className="btn-primary px-6 py-2">
|
||||||
{t('common.close')}
|
{getBaseTranslation('close', 'common.close')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -416,269 +429,106 @@ export default function Connection() {
|
|||||||
return fallback[key] || key;
|
return fallback[key] || key;
|
||||||
};
|
};
|
||||||
|
|
||||||
// App selector for RemnaWave
|
// Current platform apps for chips
|
||||||
if (showAppSelector) {
|
const currentPlatformKey = activePlatformKey || availablePlatforms[0];
|
||||||
if (!selectedPlatform) {
|
const currentPlatformData = currentPlatformKey
|
||||||
// Platform selection
|
? appConfig.platforms[currentPlatformKey]
|
||||||
const platformIcons: Record<string, React.ReactNode> = {
|
: undefined;
|
||||||
ios: (
|
const currentPlatformApps = currentPlatformData ? getRemnawaveApps(currentPlatformData) : [];
|
||||||
<svg className="h-7 w-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>
|
|
||||||
),
|
|
||||||
android: (
|
|
||||||
<svg className="h-7 w-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.24c-1.39-.59-2.94-.92-4.58-.92s-3.19.33-4.58.92L5.5 5.67c-.16-.28-.54-.37-.83-.22-.31.16-.43.54-.26.85l1.84 3.18C3.38 11.11 1.5 14.12 1.5 17.5h21c0-3.38-1.88-6.39-4.9-8.02zM7 15.25c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25zm10 0c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25z" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
windows: (
|
|
||||||
<svg className="h-6 w-6" 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>
|
|
||||||
),
|
|
||||||
macos: (
|
|
||||||
<svg className="h-7 w-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>
|
|
||||||
),
|
|
||||||
linux: (
|
|
||||||
<svg className="h-6 w-6" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M12.504 0c-.155 0-.315.008-.48.021-4.226.333-3.105 4.807-3.17 6.298-.076 1.092-.3 1.953-1.05 3.02-.885 1.051-2.127 2.75-2.716 4.521-.278.832-.41 1.684-.287 2.489.117.779.456 1.462 1.047 1.993.545.487 1.183.805 1.894.983.718.178 1.463.201 2.215.201.752 0 1.498-.023 2.215-.201.711-.178 1.349-.496 1.894-.983.591-.531.93-1.214 1.047-1.993.123-.805-.009-1.657-.287-2.489-.589-1.77-1.831-3.47-2.716-4.521-.75-1.067-.974-1.928-1.05-3.02-.065-1.491 1.056-5.965-3.17-6.298-.165-.013-.325-.021-.48-.021zm-.126 1.09c.99.063 1.783.569 2.172 1.443.376.844.413 1.903.199 2.999-.215 1.096-.687 2.23-1.357 3.18-.669.95-1.405 1.705-2.017 2.379-.611.674-1.074 1.252-1.316 1.857-.242.605-.262 1.233-.006 1.873.256.64.755 1.198 1.36 1.547.606.35 1.247.536 1.88.536.634 0 1.275-.186 1.88-.536.606-.349 1.105-.907 1.36-1.547.256-.64.236-1.268-.006-1.873-.242-.605-.705-1.183-1.316-1.857-.612-.674-1.348-1.429-2.017-2.379-.67-.95-1.142-2.084-1.357-3.18-.214-1.096-.177-2.155.199-2.999.389-.874 1.182-1.38 2.172-1.443z" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
androidTV: (
|
|
||||||
<svg className="h-7 w-7" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h5v2h8v-2h5c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 14H3V5h18v12z" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
appleTV: (
|
|
||||||
<svg className="h-7 w-7" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h5v2h8v-2h5c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 14H3V5h18v12z" />
|
|
||||||
</svg>
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
// Main RemnaWave view — inline app chips + blocks
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{!isTelegramWebApp && (
|
|
||||||
<button
|
|
||||||
onClick={handleBack}
|
|
||||||
className="-ml-2 rounded-xl p-2 text-dark-300 hover:bg-dark-800"
|
|
||||||
>
|
|
||||||
<BackIcon />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<h2 className="text-lg font-bold text-dark-100">
|
|
||||||
{t('subscription.connection.selectPlatform')}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{availablePlatforms.map((platform) => {
|
|
||||||
const platformData = appConfig.platforms[platform];
|
|
||||||
if (!platformData) return null;
|
|
||||||
const appCount = getPlatformAppsCount(platformData);
|
|
||||||
if (!appCount) return null;
|
|
||||||
const isCurrentPlatform = platform === detectedPlatform;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={platform}
|
|
||||||
onClick={() => setSelectedPlatform(platform)}
|
|
||||||
className={`flex w-full items-center gap-4 rounded-xl p-4 transition-all active:scale-[0.98] ${
|
|
||||||
isCurrentPlatform
|
|
||||||
? 'bg-accent-500/10 ring-1 ring-accent-500/30'
|
|
||||||
: 'bg-dark-800/50 hover:bg-dark-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`flex h-12 w-12 items-center justify-center rounded-xl ${
|
|
||||||
isCurrentPlatform
|
|
||||||
? 'bg-accent-500/20 text-accent-400'
|
|
||||||
: 'bg-dark-700 text-dark-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{platformIcons[platform] || <span className="text-xl">📱</span>}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 text-left">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span
|
|
||||||
className={`font-medium ${isCurrentPlatform ? 'text-accent-400' : 'text-dark-100'}`}
|
|
||||||
>
|
|
||||||
{getPlatformDisplayName(platform)}
|
|
||||||
</span>
|
|
||||||
{isCurrentPlatform && (
|
|
||||||
<span className="rounded-full bg-accent-500/10 px-2 py-0.5 text-xs text-accent-500">
|
|
||||||
{t('subscription.connection.yourDevice')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<span className="text-sm text-dark-400">
|
|
||||||
{appCount}{' '}
|
|
||||||
{appCount === 1
|
|
||||||
? t('subscription.connection.app')
|
|
||||||
: t('subscription.connection.apps')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<ChevronIcon />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// App selection for chosen platform (RemnaWave)
|
|
||||||
const platformData = appConfig.platforms[selectedPlatform];
|
|
||||||
const apps = platformData ? getRemnawaveApps(platformData) : [];
|
|
||||||
const isCurrentPlatform = selectedPlatform === detectedPlatform;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
{!isTelegramWebApp && (
|
|
||||||
<button
|
|
||||||
onClick={handleBack}
|
|
||||||
className="-ml-2 rounded-xl p-2 text-dark-300 hover:bg-dark-800"
|
|
||||||
>
|
|
||||||
<BackIcon />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<div className="flex-1">
|
|
||||||
<h2 className="text-lg font-bold text-dark-100">
|
|
||||||
{getPlatformDisplayName(selectedPlatform)}
|
|
||||||
</h2>
|
|
||||||
{isCurrentPlatform && (
|
|
||||||
<span className="text-xs text-accent-500">
|
|
||||||
{t('subscription.connection.yourDevice')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div ref={scrollContainerRef}>
|
|
||||||
<div className="grid grid-cols-3 gap-3">
|
|
||||||
{apps.map((app, idx) => {
|
|
||||||
const isSelected = selectedRemnawaveApp?.name === app.name;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={app.name + idx}
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedRemnawaveApp(app);
|
|
||||||
setActivePlatformKey(selectedPlatform);
|
|
||||||
setShowAppSelector(false);
|
|
||||||
setSelectedPlatform(null);
|
|
||||||
}}
|
|
||||||
className={`relative flex flex-col items-center rounded-xl p-4 transition-all active:scale-95 ${
|
|
||||||
isSelected
|
|
||||||
? 'bg-accent-500/15 ring-2 ring-accent-500/50'
|
|
||||||
: 'bg-dark-800/50 hover:bg-dark-800'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{app.featured && (
|
|
||||||
<div className="absolute -right-1.5 -top-1.5 flex h-6 w-6 items-center justify-center rounded-full bg-accent-500 shadow-lg">
|
|
||||||
<svg
|
|
||||||
className="h-3.5 w-3.5 text-white"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 20 20"
|
|
||||||
>
|
|
||||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={`mb-3 flex h-14 w-14 items-center justify-center rounded-2xl transition-colors ${
|
|
||||||
isSelected
|
|
||||||
? 'bg-accent-500/20 text-accent-400'
|
|
||||||
: 'bg-dark-700/80 text-dark-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{getAppIcon(app.name)}
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
className={`line-clamp-2 text-center text-sm font-medium leading-tight ${
|
|
||||||
isSelected ? 'text-accent-400' : 'text-dark-200'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{app.name}
|
|
||||||
</span>
|
|
||||||
{isSelected && (
|
|
||||||
<div className="absolute bottom-2 left-1/2 h-2 w-2 -translate-x-1/2 rounded-full bg-accent-500" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main RemnaWave view — blocks rendering
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header: title + platform dropdown */}
|
||||||
<div>
|
<div className="flex items-center gap-3">
|
||||||
<div className="mb-3 flex items-center gap-3">
|
{!isTelegramWebApp && (
|
||||||
{!isTelegramWebApp && (
|
<button
|
||||||
<button
|
onClick={handleGoBack}
|
||||||
onClick={handleGoBack}
|
className="-ml-2 rounded-xl p-2 text-dark-300 hover:bg-dark-800"
|
||||||
className="-ml-2 rounded-xl p-2 text-dark-300 hover:bg-dark-800"
|
>
|
||||||
>
|
<BackIcon />
|
||||||
<BackIcon />
|
</button>
|
||||||
</button>
|
)}
|
||||||
)}
|
<h2 className="flex-1 text-lg font-bold text-dark-100">
|
||||||
<h2 className="flex-1 text-lg font-bold text-dark-100">
|
{t('subscription.connection.title')}
|
||||||
{t('subscription.connection.title')}
|
</h2>
|
||||||
</h2>
|
{/* Platform dropdown */}
|
||||||
{/* Platform dropdown */}
|
{availablePlatforms.length > 1 && (
|
||||||
{availablePlatforms.length > 1 && (
|
<select
|
||||||
<select
|
value={currentPlatformKey || ''}
|
||||||
value={activePlatformKey || ''}
|
onChange={(e) => {
|
||||||
onChange={(e) => {
|
const newPlatform = e.target.value;
|
||||||
const newPlatform = e.target.value;
|
setActivePlatformKey(newPlatform);
|
||||||
setActivePlatformKey(newPlatform);
|
const platformData = appConfig.platforms[newPlatform];
|
||||||
const platformData = appConfig.platforms[newPlatform];
|
if (platformData) {
|
||||||
if (platformData) {
|
const apps = getRemnawaveApps(platformData);
|
||||||
const apps = getRemnawaveApps(platformData);
|
const app = apps.find((a) => a.featured) || apps[0];
|
||||||
const app = apps.find((a) => a.featured) || apps[0];
|
if (app) setSelectedRemnawaveApp(app);
|
||||||
if (app) setSelectedRemnawaveApp(app);
|
}
|
||||||
}
|
}}
|
||||||
}}
|
className="rounded-lg border border-dark-600 bg-dark-800 px-3 py-1.5 text-sm text-dark-200 outline-none"
|
||||||
className="rounded-lg border border-dark-600 bg-dark-800 px-3 py-1.5 text-sm text-dark-200 outline-none"
|
>
|
||||||
>
|
{availablePlatforms.map((p) => (
|
||||||
{availablePlatforms.map((p) => (
|
<option key={p} value={p}>
|
||||||
<option key={p} value={p}>
|
{getPlatformDisplayName(p)}
|
||||||
{getPlatformDisplayName(p)}
|
</option>
|
||||||
</option>
|
))}
|
||||||
))}
|
</select>
|
||||||
</select>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* App selector button */}
|
|
||||||
<button
|
|
||||||
onClick={() => setShowAppSelector(true)}
|
|
||||||
className="flex w-full items-center gap-3 rounded-xl bg-dark-800/50 p-3 transition-colors hover:bg-dark-800"
|
|
||||||
>
|
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-accent-500/10 text-accent-400">
|
|
||||||
{currentApp && getAppIcon(currentApp.name)}
|
|
||||||
</div>
|
|
||||||
<div className="flex-1 text-left">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="font-medium text-dark-100">{currentApp?.name}</span>
|
|
||||||
{currentApp?.featured && (
|
|
||||||
<span className="rounded-full bg-accent-500/10 px-2 py-0.5 text-xs text-accent-500">
|
|
||||||
featured
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-dark-400">{t('subscription.connection.changeApp')}</div>
|
|
||||||
</div>
|
|
||||||
<ChevronIcon />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* App chips row */}
|
||||||
|
{currentPlatformApps.length > 0 && (
|
||||||
|
<div className="-mx-1 flex flex-wrap gap-2 px-1">
|
||||||
|
{currentPlatformApps.map((app, idx) => {
|
||||||
|
const isSelected = currentApp?.name === app.name;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={app.name + idx}
|
||||||
|
onClick={() => setSelectedRemnawaveApp(app)}
|
||||||
|
className={`inline-flex items-center gap-1.5 rounded-full px-3.5 py-1.5 text-sm font-medium transition-all active:scale-95 ${
|
||||||
|
isSelected
|
||||||
|
? 'bg-accent-500/15 text-accent-400 ring-1 ring-accent-500/40'
|
||||||
|
: 'bg-dark-800/60 text-dark-300 hover:bg-dark-800'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{app.featured && (
|
||||||
|
<svg className="h-3.5 w-3.5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{app.name}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Tutorial button */}
|
||||||
|
{appConfig.baseSettings?.isShowTutorialButton && appConfig.baseSettings?.tutorialUrl && (
|
||||||
|
<a
|
||||||
|
href={appConfig.baseSettings.tutorialUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="flex items-center justify-center gap-2 rounded-xl border border-dark-600 px-4 py-2.5 text-sm font-medium text-dark-200 transition-all hover:bg-dark-800"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="h-5 w-5"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
{getBaseTranslation('tutorial', 'subscription.connection.tutorial')}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Blocks */}
|
{/* Blocks */}
|
||||||
{currentApp && (
|
{currentApp && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -686,6 +536,22 @@ export default function Connection() {
|
|||||||
const svgHtml = getSvgHtml(block.svgIconKey);
|
const svgHtml = getSvgHtml(block.svgIconKey);
|
||||||
const iconColor = block.svgIconColor || '#8B5CF6';
|
const iconColor = block.svgIconColor || '#8B5CF6';
|
||||||
|
|
||||||
|
// Fallback block title from baseTranslations by block index
|
||||||
|
const blockTitleFallbackKeys = ['installApp', 'addSubscription', 'connectAndUse'];
|
||||||
|
const blockTitleFallbackI18n = [
|
||||||
|
'subscription.connection.installApp',
|
||||||
|
'subscription.connection.addSubscription',
|
||||||
|
'subscription.connection.connectVpn',
|
||||||
|
];
|
||||||
|
const blockTitle =
|
||||||
|
getLocalizedText(block.title) ||
|
||||||
|
(blockIdx < blockTitleFallbackKeys.length
|
||||||
|
? getBaseTranslation(
|
||||||
|
blockTitleFallbackKeys[blockIdx],
|
||||||
|
blockTitleFallbackI18n[blockIdx],
|
||||||
|
)
|
||||||
|
: '');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={blockIdx} className="rounded-xl border border-dark-700 p-4">
|
<div key={blockIdx} className="rounded-xl border border-dark-700 p-4">
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
@@ -705,7 +571,7 @@ export default function Connection() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<h3 className="font-bold text-dark-100">{getLocalizedText(block.title)}</h3>
|
<h3 className="font-bold text-dark-100">{blockTitle}</h3>
|
||||||
<p className="mt-1 text-sm text-dark-400">
|
<p className="mt-1 text-sm text-dark-400">
|
||||||
{getLocalizedText(block.description)}
|
{getLocalizedText(block.description)}
|
||||||
</p>
|
</p>
|
||||||
@@ -713,24 +579,36 @@ export default function Connection() {
|
|||||||
{block.buttons && block.buttons.length > 0 && (
|
{block.buttons && block.buttons.length > 0 && (
|
||||||
<div className="mt-3 flex flex-wrap gap-2">
|
<div className="mt-3 flex flex-wrap gap-2">
|
||||||
{block.buttons.map((btn, btnIdx) => {
|
{block.buttons.map((btn, btnIdx) => {
|
||||||
const btnText = getLocalizedText(btn.text);
|
const btnText =
|
||||||
|
getLocalizedText(btn.text) ||
|
||||||
|
getBaseTranslation('openApp', 'subscription.connection.openLink');
|
||||||
|
|
||||||
if (btn.type === 'subscriptionLink') {
|
if (btn.type === 'subscriptionLink') {
|
||||||
|
// Use app deepLink first, fallback to resolved button URL
|
||||||
|
const deepLink = currentApp?.deepLink || btn.resolvedUrl;
|
||||||
|
if (!deepLink || !isValidDeepLink(deepLink)) return null;
|
||||||
|
const subBtnSvg = getSvgHtml(btn.svgIconKey);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={btnIdx}
|
key={btnIdx}
|
||||||
onClick={() => {
|
onClick={() => openDeepLink(deepLink)}
|
||||||
if (currentApp) handleConnectRemnawave(currentApp);
|
|
||||||
}}
|
|
||||||
className="btn-primary flex items-center gap-2 px-4 py-2 text-sm font-semibold"
|
className="btn-primary flex items-center gap-2 px-4 py-2 text-sm font-semibold"
|
||||||
>
|
>
|
||||||
<LinkIcon />
|
{subBtnSvg ? (
|
||||||
|
<div
|
||||||
|
className="h-5 w-5 [&>svg]:h-full [&>svg]:w-full"
|
||||||
|
dangerouslySetInnerHTML={{ __html: subBtnSvg }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LinkIcon />
|
||||||
|
)}
|
||||||
{btnText}
|
{btnText}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (btn.type === 'copyButton') {
|
if (btn.type === 'copyButton') {
|
||||||
|
if (appConfig?.hideLink) return null;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={btnIdx}
|
key={btnIdx}
|
||||||
@@ -742,7 +620,12 @@ export default function Connection() {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{copied ? <CheckIcon /> : <CopyIcon />}
|
{copied ? <CheckIcon /> : <CopyIcon />}
|
||||||
{copied ? t('subscription.connection.copied') : btnText}
|
{copied
|
||||||
|
? t('subscription.connection.copied')
|
||||||
|
: getBaseTranslation(
|
||||||
|
'copyLink',
|
||||||
|
'subscription.connection.copyLink',
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user