style: refine Linux documentation UI with manual steps and dynamic CLI selector

This commit is contained in:
dyzulk
2026-01-06 15:22:49 +07:00
parent 74e1a2cb93
commit 33ba0d5e7e
3 changed files with 100 additions and 52 deletions

View File

@@ -91,45 +91,93 @@ function TabButton({ id, label, active, onClick, icon }: { id: string, label: st
);
}
function OsGuideContent({ title, steps }: { title: string, steps: string[] }) {
function OsGuideContent({ title, steps, selectedOs, certificates, t }: { title: string, steps: string[], selectedOs: string, certificates: CaCertificate[], t: any }) {
const [guideCertSerial, setGuideCertSerial] = useState(certificates[0]?.serial);
const [copied, setCopied] = useState(false);
const activeCert = certificates.find(c => c.serial === guideCertSerial) || certificates[0];
const linuxCli = activeCert?.linux_cdn_url ? `curl -sL ${activeCert.linux_cdn_url} | sudo bash` : '';
const copyCli = () => {
if (!linuxCli) return;
navigator.clipboard.writeText(linuxCli);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="p-6 bg-white dark:bg-gray-800 rounded-2xl border border-gray-100 dark:border-gray-700 shadow-sm">
<h4 className="text-lg font-bold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
<div className="w-1.5 h-6 bg-brand-500 rounded-full"></div>
{title}
</h4>
<ul className="space-y-4">
{steps.map((step, idx) => {
const isCommand = step.includes('curl') || step.includes('bash') || step.includes('sudo');
return (
<div className="p-8 bg-white dark:bg-gray-800 rounded-[2rem] border border-gray-100 dark:border-gray-700 shadow-sm space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-500">
<div>
<h4 className="text-lg font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2">
<div className="w-1.5 h-6 bg-brand-500 rounded-full"></div>
{title}
</h4>
<ul className="space-y-4">
{steps.map((step, idx) => (
<li key={idx} className="flex gap-4">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-brand-50 dark:bg-brand-500/10 text-brand-600 dark:text-brand-400 flex items-center justify-center text-xs font-bold border border-brand-100 dark:border-brand-500/20">
{idx + 1}
</span>
<div className="flex-1">
<p className={`text-sm text-gray-600 dark:text-gray-400 leading-relaxed ${isCommand ? 'font-mono bg-gray-100 dark:bg-gray-900 px-3 py-1.5 rounded-lg border border-gray-200 dark:border-gray-700 break-all' : 'font-medium'}`}>
{step}
</p>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed font-medium">
{step}
</p>
</li>
);
})}
</ul>
))}
</ul>
</div>
{selectedOs === 'linux' && (
<div className="pt-8 border-t border-gray-100 dark:border-gray-700 space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h5 className="font-bold text-gray-900 dark:text-white flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span>
{t('guide_linux_shortcut_title')}
</h5>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">{t('guide_linux_shortcut_desc')}</p>
</div>
{/* Cert Selector */}
<div className="relative group min-w-[200px]">
<select
value={guideCertSerial}
onChange={(e) => setGuideCertSerial(e.target.value)}
className="w-full appearance-none bg-gray-50 dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl px-4 py-2 pr-10 text-xs font-bold text-gray-700 dark:text-gray-300 focus:outline-none focus:ring-2 focus:ring-brand-500/20 transition-all cursor-pointer"
>
{certificates.map(c => (
<option key={c.serial} value={c.serial}>{c.name}</option>
))}
</select>
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none text-gray-400">
<ChevronDownIcon className="w-4 h-4" />
</div>
</div>
</div>
<div className="relative group">
<div className="font-mono text-xs bg-gray-950 text-brand-400 p-4 rounded-2xl border border-gray-800 break-all pr-12">
<span className="text-gray-500 mr-2">$</span>
{linuxCli || 'No Linux script available for this certificate.'}
</div>
<button
onClick={copyCli}
className={`absolute right-3 top-1/2 -translate-y-1/2 p-2 rounded-xl transition-all duration-300 active:scale-90 ${
copied
? 'bg-green-500 text-white'
: 'bg-gray-800 text-gray-400 hover:text-white hover:bg-gray-700'
}`}
title="Copy Command"
>
{copied ? <CheckIcon className="w-3.5 h-3.5" /> : <CopyIcon className="w-3.5 h-3.5" />}
</button>
</div>
</div>
)}
</div>
);
}
function CaCard({ cert, isRoot, t, selectedOs, setSelectedOs }: { cert: CaCertificate, isRoot: boolean, t: any, selectedOs: string, setSelectedOs: (os: any) => void }) {
const [copied, setCopied] = useState(false);
const copyLinuxCli = () => {
if (!cert.linux_cdn_url) return;
const command = `curl -sL ${cert.linux_cdn_url} | sudo bash`;
navigator.clipboard.writeText(command);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<div className="group relative bg-white/40 dark:bg-gray-900/40 backdrop-blur-xl rounded-[2.5rem] p-8 border border-white/50 dark:border-gray-800/50 shadow-xl hover:shadow-2xl transition-all duration-500 hover:-translate-y-1">
{/* Sync Status Badge */}
@@ -198,7 +246,7 @@ function CaCard({ cert, isRoot, t, selectedOs, setSelectedOs }: { cert: CaCertif
</div>
{cert.linux_cdn_url && (
<div className="mt-4 flex gap-3">
<div className="mt-4">
<DownloadBtn
href={cert.linux_cdn_url || `/api/public/ca-certificates/${cert.serial}/download/linux`}
label={t('download_linux') || "Linux (.sh)"}
@@ -207,18 +255,6 @@ function CaCard({ cert, isRoot, t, selectedOs, setSelectedOs }: { cert: CaCertif
variant="gray"
isFullWidth
/>
<button
onClick={copyLinuxCli}
className={`flex items-center justify-center gap-2 px-6 py-3 rounded-2xl border font-bold text-xs transition-all duration-300 active:scale-95 ${
copied
? 'bg-green-500 text-white border-green-500 shadow-lg'
: 'bg-brand-500 text-white border-brand-500 hover:bg-brand-600 shadow-md hover:shadow-xl'
}`}
title="Copy CLI Command"
>
{copied ? <CheckIcon className="w-4 h-4" /> : <CopyIcon className="w-4 h-4" />}
{copied ? (t('copied') || "Copied!") : (t('copy_cli') || "Copy CLI")}
</button>
</div>
)}
</div>
@@ -290,6 +326,12 @@ const CheckIcon = ({ className }: { className?: string }) => (
</svg>
);
const ChevronDownIcon = ({ className }: { className?: string }) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7" />
</svg>
);
export default function HomeClient() {
const t = useTranslations("Home");
const [certificates, setCertificates] = useState<CaCertificate[]>([]);
@@ -503,11 +545,11 @@ export default function HomeClient() {
<TabButton id="mobile" label="Android/iOS" active={selectedOs === 'mobile'} onClick={() => setSelectedOs('mobile')} icon={<MobileIcon className="w-4 h-4" />} />
</div>
<div className="space-y-4 animate-in fade-in slide-in-from-bottom-2 duration-500">
{selectedOs === 'windows' && <OsGuideContent title="Windows Installation" steps={t.raw('guide_steps_windows')} />}
{selectedOs === 'macos' && <OsGuideContent title="macOS Installation" steps={t.raw('guide_steps_macos')} />}
{selectedOs === 'linux' && <OsGuideContent title="Linux Installation" steps={t.raw('guide_steps_linux')} />}
{selectedOs === 'mobile' && <OsGuideContent title="Mobile Installation" steps={t.raw('guide_steps_mobile')} />}
<div className="space-y-4">
{selectedOs === 'windows' && <OsGuideContent title="Windows Installation" steps={t.raw('guide_steps_windows')} selectedOs={selectedOs} certificates={certificates} t={t} />}
{selectedOs === 'macos' && <OsGuideContent title="macOS Installation" steps={t.raw('guide_steps_macos')} selectedOs={selectedOs} certificates={certificates} t={t} />}
{selectedOs === 'linux' && <OsGuideContent title="Linux Installation" steps={t.raw('guide_steps_linux')} selectedOs={selectedOs} certificates={certificates} t={t} />}
{selectedOs === 'mobile' && <OsGuideContent title="Mobile Installation" steps={t.raw('guide_steps_mobile')} selectedOs={selectedOs} certificates={certificates} t={t} />}
</div>
</div>
</div>

View File

@@ -212,10 +212,13 @@
"Go to 'About' > 'Certificate Trust Settings' and enable full trust."
],
"guide_steps_linux": [
"Use the one-liner command for instant setup (recommended):",
"curl -sL [URL] | sudo bash",
"Note: Get the [URL] by right-clicking the 'Linux' download button on the certificate card above."
"Download the .sh installer script.",
"Open terminal and run: sudo bash install-*.sh",
"The script will automatically detect and update your CA store."
],
"guide_linux_shortcut_title": "One-liner Shortcut (Recommended)",
"guide_linux_shortcut_desc": "Copy and paste this command to your terminal for instant installation:",
"guide_linux_select_cert": "Select Certificate to install:",
"guide_steps_mobile": [
"Android: Settings > Security > Install from storage > CA Certificate.",
"iOS: Install the profile, then Settings > General > About > Certificate Trust Settings."

View File

@@ -212,10 +212,13 @@
"Buka 'About' > 'Certificate Trust Settings' dan aktifkan kepercayaan penuh."
],
"guide_steps_linux": [
"Gunakan perintah satu-baris untuk instalasi instan (disarankan):",
"curl -sL [URL] | sudo bash",
"Catatan: Dapatkan [URL] dengan klik kanan tombol 'Linux' pada kartu sertifikat di atas lalu 'Salin Alamat Link'."
"Unduh skrip penginstal .sh.",
"Buka terminal dan jalankan: sudo bash install-*.sh",
"Skrip akan secara otomatis mendeteksi distro dan memperbarui penyimpanan CA Anda."
],
"guide_linux_shortcut_title": "Pintas Satu-Baris (Disarankan)",
"guide_linux_shortcut_desc": "Salin dan tempel perintah ini ke terminal Anda untuk instalasi instan:",
"guide_linux_select_cert": "Pilih Sertifikat untuk diinstal:",
"guide_steps_mobile": [
"Android: Pengaturan > Keamanan > Instal dari penyimpanan > Sertifikat CA.",
"iOS: Instal profil, lalu Pengaturan > Umum > Mengenai > Pengaturan Kepercayaan Sertifikat."