mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 02:23:47 +00:00
refactor: extract BlockButtons component from InstallationGuide
Move button rendering logic (URL validation, copy handler, icon components) into a dedicated BlockButtons component. Remove duplicate getLocalizedText/getBaseTranslation helpers from Connection page controller.
This commit is contained in:
153
src/components/connection/blocks/BlockButtons.tsx
Normal file
153
src/components/connection/blocks/BlockButtons.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { RemnawaveButtonClient, LocalizedText } from '@/types';
|
||||
|
||||
// eslint-disable-next-line no-script-url
|
||||
const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:'];
|
||||
|
||||
function isValidDeepLink(url: string | undefined): boolean {
|
||||
if (!url) return false;
|
||||
const lowerUrl = url.toLowerCase().trim();
|
||||
if (dangerousSchemes.some((s) => lowerUrl.startsWith(s))) return false;
|
||||
return lowerUrl.includes('://');
|
||||
}
|
||||
|
||||
function isValidExternalUrl(url: string | undefined): boolean {
|
||||
if (!url) return false;
|
||||
const lowerUrl = url.toLowerCase().trim();
|
||||
if (dangerousSchemes.some((s) => lowerUrl.startsWith(s))) return false;
|
||||
return lowerUrl.startsWith('http://') || lowerUrl.startsWith('https://');
|
||||
}
|
||||
|
||||
const CopyIcon = () => (
|
||||
<svg className="h-4 w-4" 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="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
interface BlockButtonsProps {
|
||||
buttons: RemnawaveButtonClient[] | undefined;
|
||||
variant: 'light' | 'subtle';
|
||||
subscriptionUrl: string | null;
|
||||
hideLink?: boolean;
|
||||
deepLink?: string | null;
|
||||
getLocalizedText: (text: LocalizedText | undefined) => string;
|
||||
getBaseTranslation: (key: string, i18nKey: string) => string;
|
||||
getSvgHtml: (key: string | undefined) => string;
|
||||
onOpenDeepLink: (url: string) => void;
|
||||
}
|
||||
|
||||
export function BlockButtons({
|
||||
buttons,
|
||||
variant,
|
||||
subscriptionUrl,
|
||||
hideLink,
|
||||
deepLink,
|
||||
getLocalizedText,
|
||||
getBaseTranslation,
|
||||
getSvgHtml,
|
||||
onOpenDeepLink,
|
||||
}: BlockButtonsProps) {
|
||||
const { t } = useTranslation();
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
const handleCopy = useCallback(async (url: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(url);
|
||||
} catch {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = url;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}, []);
|
||||
|
||||
if (!buttons || buttons.length === 0) return null;
|
||||
|
||||
const baseClass =
|
||||
variant === 'light'
|
||||
? 'rounded-xl border border-accent-500/40 px-4 py-2 text-sm font-medium text-accent-400 transition-all hover:bg-accent-500/10'
|
||||
: 'rounded-xl px-3 py-1.5 text-sm font-medium text-dark-300 transition-all hover:bg-dark-700/50';
|
||||
|
||||
return (
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
{buttons.map((btn, idx) => {
|
||||
const btnText = getLocalizedText(btn.text);
|
||||
const btnSvg = getSvgHtml(btn.svgIconKey);
|
||||
const btnIcon = btnSvg ? (
|
||||
<div
|
||||
className="h-4 w-4 [&>svg]:h-full [&>svg]:w-full"
|
||||
dangerouslySetInnerHTML={{ __html: btnSvg }}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
if (btn.type === 'subscriptionLink') {
|
||||
const url = btn.resolvedUrl || btn.url || btn.link || deepLink || subscriptionUrl;
|
||||
if (!url || !isValidDeepLink(url)) return null;
|
||||
return (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => onOpenDeepLink(url)}
|
||||
className={`flex items-center gap-2 ${baseClass}`}
|
||||
>
|
||||
{btnIcon}
|
||||
{btnText || getBaseTranslation('openApp', 'subscription.connection.openLink')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (btn.type === 'copyButton') {
|
||||
if (hideLink) return null;
|
||||
const url = btn.resolvedUrl || subscriptionUrl;
|
||||
if (!url) return null;
|
||||
return (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => handleCopy(url)}
|
||||
className={`flex items-center gap-2 ${
|
||||
copied
|
||||
? 'rounded-xl border border-success-500 bg-success-500/10 px-4 py-2 text-sm font-medium text-success-400'
|
||||
: baseClass
|
||||
}`}
|
||||
>
|
||||
{copied ? <CheckIcon /> : btnIcon || <CopyIcon />}
|
||||
{copied
|
||||
? t('subscription.connection.copied')
|
||||
: btnText || getBaseTranslation('copyLink', 'subscription.connection.copyLink')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
// external
|
||||
const href = btn.link || btn.url || '';
|
||||
if (!isValidExternalUrl(href)) return null;
|
||||
return (
|
||||
<a
|
||||
key={idx}
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={`inline-flex items-center gap-2 ${baseClass}`}
|
||||
>
|
||||
{btnIcon}
|
||||
{btnText}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user