mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-29 18:13:47 +00:00
refactor: simplify AdminApps page to config selector and compact app chips
- Rewrite AdminApps.tsx from ~560 to ~165 lines: status card, config list, UUID input, save/disconnect - Remove legacy types and conversion functions from adminApps.ts (~370 lines) - Connection.tsx: flex-wrap app chips, reduce padding and icon size for compact layout
This commit is contained in:
@@ -8,47 +8,6 @@ export interface LocalizedText {
|
||||
fr?: string;
|
||||
}
|
||||
|
||||
export interface AppButton {
|
||||
id?: string; // Unique identifier for React key (client-side only)
|
||||
buttonLink: string;
|
||||
buttonText: LocalizedText;
|
||||
}
|
||||
|
||||
export interface AppStep {
|
||||
description: LocalizedText;
|
||||
buttons?: AppButton[];
|
||||
title?: LocalizedText;
|
||||
}
|
||||
|
||||
export interface AppDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
isFeatured: boolean;
|
||||
urlScheme: string;
|
||||
isNeedBase64Encoding?: boolean;
|
||||
installationStep: AppStep;
|
||||
addSubscriptionStep: AppStep;
|
||||
connectAndUseStep: AppStep;
|
||||
additionalBeforeAddSubscriptionStep?: AppStep;
|
||||
additionalAfterAddSubscriptionStep?: AppStep;
|
||||
}
|
||||
|
||||
export interface AppConfigBranding {
|
||||
name: string;
|
||||
logoUrl: string;
|
||||
supportUrl: string;
|
||||
}
|
||||
|
||||
export interface AppConfigConfig {
|
||||
additionalLocales: string[];
|
||||
branding: AppConfigBranding;
|
||||
}
|
||||
|
||||
export interface AppConfigResponse {
|
||||
config: AppConfigConfig;
|
||||
platforms: Record<string, AppDefinition[]>;
|
||||
}
|
||||
|
||||
export const adminAppsApi = {
|
||||
// Get RemnaWave config status
|
||||
getRemnaWaveStatus: async (): Promise<{ enabled: boolean; config_uuid: string | null }> => {
|
||||
@@ -155,332 +114,3 @@ export interface RemnawaveConfig {
|
||||
baseTranslations?: RemnawaveBaseTranslations;
|
||||
brandingSettings?: RemnawaveBrandingSettings;
|
||||
}
|
||||
|
||||
// ============== Converter Functions ==============
|
||||
|
||||
const emptyLocalizedText = (): LocalizedText => ({
|
||||
en: '',
|
||||
ru: '',
|
||||
zh: '',
|
||||
fa: '',
|
||||
fr: '',
|
||||
});
|
||||
|
||||
// Convert Cabinet button to RemnaWave button
|
||||
const cabinetButtonToRemnawave = (button: AppButton): RemnawaveButton => ({
|
||||
url: button.buttonLink,
|
||||
text: { ...emptyLocalizedText(), ...button.buttonText },
|
||||
});
|
||||
|
||||
// Convert RemnaWave button to Cabinet button
|
||||
const remnawaveButtonToCabinet = (button: RemnawaveButton): AppButton => ({
|
||||
buttonLink: button.url,
|
||||
buttonText: {
|
||||
en: button.text.en || '',
|
||||
ru: button.text.ru || '',
|
||||
zh: button.text.zh,
|
||||
fa: button.text.fa,
|
||||
fr: button.text.fr,
|
||||
},
|
||||
});
|
||||
|
||||
// Convert Cabinet app to RemnaWave app format
|
||||
export const cabinetAppToRemnawave = (app: AppDefinition): RemnawaveApp => {
|
||||
const blocks: RemnawaveBlock[] = [];
|
||||
|
||||
// Block 1: Installation
|
||||
blocks.push({
|
||||
title: { ...emptyLocalizedText(), en: 'Install App', ru: 'Установка приложения' },
|
||||
description: { ...emptyLocalizedText(), ...app.installationStep.description },
|
||||
buttons: app.installationStep.buttons?.map(cabinetButtonToRemnawave),
|
||||
svgIconKey: 'download',
|
||||
svgIconColor: '#3B82F6',
|
||||
});
|
||||
|
||||
// Block 2 (optional): Additional before subscription
|
||||
if (
|
||||
app.additionalBeforeAddSubscriptionStep?.description?.en ||
|
||||
app.additionalBeforeAddSubscriptionStep?.description?.ru
|
||||
) {
|
||||
blocks.push({
|
||||
title: app.additionalBeforeAddSubscriptionStep.title || {
|
||||
...emptyLocalizedText(),
|
||||
en: 'Preparation',
|
||||
ru: 'Подготовка',
|
||||
},
|
||||
description: {
|
||||
...emptyLocalizedText(),
|
||||
...app.additionalBeforeAddSubscriptionStep.description,
|
||||
},
|
||||
buttons: app.additionalBeforeAddSubscriptionStep.buttons?.map(cabinetButtonToRemnawave),
|
||||
svgIconKey: 'settings',
|
||||
svgIconColor: '#8B5CF6',
|
||||
});
|
||||
}
|
||||
|
||||
// Block 3: Add subscription
|
||||
blocks.push({
|
||||
title: { ...emptyLocalizedText(), en: 'Add Subscription', ru: 'Добавить подписку' },
|
||||
description: { ...emptyLocalizedText(), ...app.addSubscriptionStep.description },
|
||||
buttons: app.addSubscriptionStep.buttons?.map(cabinetButtonToRemnawave),
|
||||
svgIconKey: 'plus',
|
||||
svgIconColor: '#10B981',
|
||||
});
|
||||
|
||||
// Block 4 (optional): Additional after subscription
|
||||
if (
|
||||
app.additionalAfterAddSubscriptionStep?.description?.en ||
|
||||
app.additionalAfterAddSubscriptionStep?.description?.ru
|
||||
) {
|
||||
blocks.push({
|
||||
title: app.additionalAfterAddSubscriptionStep.title || {
|
||||
...emptyLocalizedText(),
|
||||
en: 'Configuration',
|
||||
ru: 'Настройка',
|
||||
},
|
||||
description: {
|
||||
...emptyLocalizedText(),
|
||||
...app.additionalAfterAddSubscriptionStep.description,
|
||||
},
|
||||
buttons: app.additionalAfterAddSubscriptionStep.buttons?.map(cabinetButtonToRemnawave),
|
||||
svgIconKey: 'settings',
|
||||
svgIconColor: '#F59E0B',
|
||||
});
|
||||
}
|
||||
|
||||
// Block 5: Connect and use
|
||||
blocks.push({
|
||||
title: { ...emptyLocalizedText(), en: 'Connect and Use', ru: 'Подключение и использование' },
|
||||
description: { ...emptyLocalizedText(), ...app.connectAndUseStep.description },
|
||||
buttons: app.connectAndUseStep.buttons?.map(cabinetButtonToRemnawave),
|
||||
svgIconKey: 'check',
|
||||
svgIconColor: '#22C55E',
|
||||
});
|
||||
|
||||
return {
|
||||
name: app.name,
|
||||
featured: app.isFeatured,
|
||||
urlScheme: app.urlScheme,
|
||||
isNeedBase64Encoding: app.isNeedBase64Encoding,
|
||||
blocks,
|
||||
};
|
||||
};
|
||||
|
||||
// Convert RemnaWave app to Cabinet app format
|
||||
export const remnawaveAppToCabinet = (app: RemnawaveApp, platform: string): AppDefinition => {
|
||||
const blocks = app.blocks || [];
|
||||
|
||||
// Default empty step
|
||||
const emptyStep = (): AppStep => ({
|
||||
description: emptyLocalizedText(),
|
||||
buttons: [],
|
||||
});
|
||||
|
||||
// Try to identify blocks by their titles or position
|
||||
let installationBlock: RemnawaveBlock | undefined;
|
||||
let subscriptionBlock: RemnawaveBlock | undefined;
|
||||
let connectBlock: RemnawaveBlock | undefined;
|
||||
let beforeSubBlock: RemnawaveBlock | undefined;
|
||||
let afterSubBlock: RemnawaveBlock | undefined;
|
||||
|
||||
// First pass: try to identify by title keywords
|
||||
for (const block of blocks) {
|
||||
const enTitle = (block.title?.en || '').toLowerCase();
|
||||
const ruTitle = (block.title?.ru || '').toLowerCase();
|
||||
|
||||
if (enTitle.includes('install') || ruTitle.includes('установ') || ruTitle.includes('скачай')) {
|
||||
if (!installationBlock) installationBlock = block;
|
||||
} else if (
|
||||
enTitle.includes('subscription') ||
|
||||
enTitle.includes('add') ||
|
||||
ruTitle.includes('подписк') ||
|
||||
ruTitle.includes('добав')
|
||||
) {
|
||||
if (!subscriptionBlock) subscriptionBlock = block;
|
||||
} else if (
|
||||
enTitle.includes('connect') ||
|
||||
enTitle.includes('use') ||
|
||||
ruTitle.includes('подключ') ||
|
||||
ruTitle.includes('использ')
|
||||
) {
|
||||
if (!connectBlock) connectBlock = block;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: assign remaining blocks by position if not found by title
|
||||
if (!installationBlock && blocks.length > 0) {
|
||||
installationBlock = blocks[0];
|
||||
}
|
||||
if (!subscriptionBlock && blocks.length > 1) {
|
||||
subscriptionBlock = blocks.find((b) => b !== installationBlock) || blocks[1];
|
||||
}
|
||||
if (!connectBlock && blocks.length > 2) {
|
||||
connectBlock =
|
||||
blocks.find((b) => b !== installationBlock && b !== subscriptionBlock) ||
|
||||
blocks[blocks.length - 1];
|
||||
}
|
||||
|
||||
// Assign additional blocks
|
||||
const additionalBlocks = blocks.filter(
|
||||
(b) => b !== installationBlock && b !== subscriptionBlock && b !== connectBlock,
|
||||
);
|
||||
if (additionalBlocks.length >= 1) {
|
||||
// Check if block appears before subscription
|
||||
const subIndex = blocks.indexOf(subscriptionBlock!);
|
||||
const firstAdditionalIndex = blocks.indexOf(additionalBlocks[0]);
|
||||
if (firstAdditionalIndex < subIndex) {
|
||||
beforeSubBlock = additionalBlocks[0];
|
||||
if (additionalBlocks.length >= 2) {
|
||||
afterSubBlock = additionalBlocks[1];
|
||||
}
|
||||
} else {
|
||||
afterSubBlock = additionalBlocks[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Convert block to cabinet step
|
||||
const blockToStep = (block: RemnawaveBlock | undefined): AppStep => {
|
||||
if (!block) return emptyStep();
|
||||
return {
|
||||
description: {
|
||||
en: block.description?.en || '',
|
||||
ru: block.description?.ru || '',
|
||||
zh: block.description?.zh,
|
||||
fa: block.description?.fa,
|
||||
fr: block.description?.fr,
|
||||
},
|
||||
title: block.title
|
||||
? {
|
||||
en: block.title.en || '',
|
||||
ru: block.title.ru || '',
|
||||
zh: block.title.zh,
|
||||
fa: block.title.fa,
|
||||
fr: block.title.fr,
|
||||
}
|
||||
: undefined,
|
||||
buttons: block.buttons?.map(remnawaveButtonToCabinet),
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
id: `${app.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}-${platform}-${Date.now()}`,
|
||||
name: app.name,
|
||||
isFeatured: app.featured || false,
|
||||
urlScheme: app.urlScheme || '',
|
||||
isNeedBase64Encoding: app.isNeedBase64Encoding,
|
||||
installationStep: blockToStep(installationBlock),
|
||||
addSubscriptionStep: blockToStep(subscriptionBlock),
|
||||
connectAndUseStep: blockToStep(connectBlock),
|
||||
additionalBeforeAddSubscriptionStep: beforeSubBlock ? blockToStep(beforeSubBlock) : undefined,
|
||||
additionalAfterAddSubscriptionStep: afterSubBlock ? blockToStep(afterSubBlock) : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
// Export all apps from cabinet to RemnaWave format
|
||||
export const exportToRemnawaveFormat = (
|
||||
platformApps: Record<string, AppDefinition[]>,
|
||||
branding?: AppConfigBranding,
|
||||
): RemnawaveConfig => {
|
||||
const platforms: Record<string, RemnawavePlatform> = {};
|
||||
|
||||
for (const [platform, apps] of Object.entries(platformApps)) {
|
||||
platforms[platform] = {
|
||||
apps: apps.map(cabinetAppToRemnawave),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
platforms,
|
||||
svgLibrary: {
|
||||
download: {
|
||||
svgString:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" /></svg>',
|
||||
},
|
||||
plus: {
|
||||
svgString:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" /></svg>',
|
||||
},
|
||||
check: {
|
||||
svgString:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" /></svg>',
|
||||
},
|
||||
settings: {
|
||||
svgString:
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>',
|
||||
},
|
||||
},
|
||||
baseSettings: {
|
||||
isShowTutorialButton: false,
|
||||
tutorialUrl: '',
|
||||
},
|
||||
baseTranslations: {
|
||||
installApp: {
|
||||
en: 'Install App',
|
||||
ru: 'Установить приложение',
|
||||
zh: '安装应用',
|
||||
fa: 'نصب برنامه',
|
||||
fr: "Installer l'application",
|
||||
},
|
||||
addSubscription: {
|
||||
en: 'Add Subscription',
|
||||
ru: 'Добавить подписку',
|
||||
zh: '添加订阅',
|
||||
fa: 'اضافه کردن اشتراک',
|
||||
fr: 'Ajouter un abonnement',
|
||||
},
|
||||
connectAndUse: {
|
||||
en: 'Connect and Use',
|
||||
ru: 'Подключиться и использовать',
|
||||
zh: '连接并使用',
|
||||
fa: 'اتصال و استفاده',
|
||||
fr: 'Connecter et utiliser',
|
||||
},
|
||||
copyLink: {
|
||||
en: 'Copy Link',
|
||||
ru: 'Скопировать ссылку',
|
||||
zh: '复制链接',
|
||||
fa: 'کپی لینک',
|
||||
fr: 'Copier le lien',
|
||||
},
|
||||
openApp: {
|
||||
en: 'Open App',
|
||||
ru: 'Открыть приложение',
|
||||
zh: '打开应用',
|
||||
fa: 'باز کردن برنامه',
|
||||
fr: "Ouvrir l'application",
|
||||
},
|
||||
tutorial: { en: 'Tutorial', ru: 'Инструкция', zh: '教程', fa: 'آموزش', fr: 'Tutoriel' },
|
||||
close: { en: 'Close', ru: 'Закрыть', zh: '关闭', fa: 'بستن', fr: 'Fermer' },
|
||||
},
|
||||
brandingSettings: branding
|
||||
? {
|
||||
name: branding.name,
|
||||
logoUrl: branding.logoUrl,
|
||||
supportUrl: branding.supportUrl,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
};
|
||||
|
||||
// Import apps from RemnaWave format to cabinet
|
||||
export const importFromRemnawaveFormat = (
|
||||
config: RemnawaveConfig,
|
||||
): { platformApps: Record<string, AppDefinition[]>; branding?: AppConfigBranding } => {
|
||||
const platformApps: Record<string, AppDefinition[]> = {};
|
||||
|
||||
for (const [platform, platformData] of Object.entries(config.platforms || {})) {
|
||||
platformApps[platform] = (platformData.apps || []).map((app) =>
|
||||
remnawaveAppToCabinet(app, platform),
|
||||
);
|
||||
}
|
||||
|
||||
const branding = config.brandingSettings
|
||||
? {
|
||||
name: config.brandingSettings.name,
|
||||
logoUrl: config.brandingSettings.logoUrl,
|
||||
supportUrl: config.brandingSettings.supportUrl,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return { platformApps, branding };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user