mirror of
https://github.com/dyzulk/trustlab.git
synced 2026-01-26 05:25:36 +07:00
715 lines
48 KiB
TypeScript
715 lines
48 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import CommonGridShape from "@/components/common/CommonGridShape";
|
|
import { useEffect, useState } from "react";
|
|
import axios from '@/lib/axios';
|
|
import { useTranslations } from "next-intl";
|
|
|
|
interface CaCertificate {
|
|
name: string;
|
|
type: string;
|
|
serial: string;
|
|
family_id?: string | null;
|
|
expires_at: string;
|
|
last_synced_at?: string | null;
|
|
cdn_url?: string | null;
|
|
der_cdn_url?: string | null;
|
|
bat_cdn_url?: string | null;
|
|
mac_cdn_url?: string | null;
|
|
linux_cdn_url?: string | null;
|
|
}
|
|
|
|
// Simple internal ScrollToTop component
|
|
function ScrollToTop() {
|
|
const [show, setShow] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const handleScroll = () => {
|
|
setShow(window.pageYOffset > 500);
|
|
};
|
|
window.addEventListener('scroll', handleScroll);
|
|
return () => window.removeEventListener('scroll', handleScroll);
|
|
}, []);
|
|
|
|
const scrollToTop = () => {
|
|
const element = document.querySelector('#home');
|
|
if (element) {
|
|
const navbarOffset = 80;
|
|
const elementPosition = element.getBoundingClientRect().top;
|
|
const offsetPosition = elementPosition + window.pageYOffset - navbarOffset;
|
|
window.scrollTo({
|
|
top: offsetPosition,
|
|
behavior: "smooth",
|
|
});
|
|
}
|
|
};
|
|
|
|
if (!show) return null;
|
|
|
|
return (
|
|
<button
|
|
onClick={scrollToTop}
|
|
className="fixed bottom-8 right-8 z-50 p-4 bg-brand-500 hover:bg-brand-600 text-white rounded-2xl shadow-2xl shadow-brand-500/40 transition-all hover:-translate-y-1 active:scale-95 animate-in fade-in slide-in-from-bottom-5"
|
|
aria-label="Back to top"
|
|
>
|
|
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
|
|
</svg>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
// UI Sub-components
|
|
function Badge({ children, variant = 'brand' }: { children: React.ReactNode, variant?: 'brand' | 'blue' | 'purple' | 'green' | 'gray' }) {
|
|
const variants = {
|
|
brand: 'bg-brand-50 text-brand-700 dark:bg-brand-500/10 dark:text-brand-400 border-brand-100 dark:border-brand-500/20',
|
|
blue: 'bg-blue-50 text-blue-700 dark:bg-blue-500/10 dark:text-blue-400 border-blue-100 dark:border-blue-500/20',
|
|
purple: 'bg-purple-50 text-purple-700 dark:bg-purple-500/10 dark:text-purple-400 border-purple-100 dark:border-purple-500/20',
|
|
green: 'bg-green-50 text-green-700 dark:bg-green-500/10 dark:text-green-400 border-green-100 dark:border-green-500/20',
|
|
gray: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-400 border-gray-200 dark:border-gray-700',
|
|
};
|
|
return (
|
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-bold uppercase tracking-wider border ${variants[variant]}`}>
|
|
{children}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function TabButton({ id, label, active, onClick, icon }: { id: string, label: string, active: boolean, onClick: () => void, icon?: React.ReactNode }) {
|
|
return (
|
|
<button
|
|
onClick={onClick}
|
|
className={`flex items-center gap-2 px-5 py-2.5 rounded-xl text-sm font-bold transition-all duration-300 ${
|
|
active
|
|
? 'bg-white dark:bg-gray-700 text-brand-600 dark:text-brand-400 shadow-sm'
|
|
: 'text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200'
|
|
}`}
|
|
>
|
|
{icon}
|
|
{label}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
function CliSnippet({ label, command, t }: { label: string, command: string, t: any }) {
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
const copyCli = () => {
|
|
if (!command) return;
|
|
navigator.clipboard.writeText(command);
|
|
setCopied(true);
|
|
setTimeout(() => setCopied(false), 2000);
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center justify-between gap-4">
|
|
<span className="text-[10px] font-bold text-gray-500 dark:text-gray-400 uppercase tracking-widest">{label}</span>
|
|
</div>
|
|
<div className="relative group">
|
|
<div className="font-mono text-[11px] bg-gray-950 text-brand-400 p-4 rounded-2xl border border-gray-800 break-all pr-12 shadow-inner">
|
|
<span className="text-gray-600 mr-2 select-none">$</span>
|
|
{command || t('no_script')}
|
|
</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 border ${
|
|
copied
|
|
? 'bg-green-500 text-white border-green-500 shadow-lg shadow-green-500/20'
|
|
: 'bg-gray-900 text-gray-400 hover:text-white border-gray-800 hover:bg-gray-800'
|
|
}`}
|
|
title={t('copy_command')}
|
|
>
|
|
{copied ? <CheckIcon className="w-3.5 h-3.5" /> : <CopyIcon className="w-3.5 h-3.5" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function OsGuideContent({ title, steps, selectedOs, certificates, t, linuxDistro, setLinuxDistro }: { title: string, steps: string[], selectedOs: string, certificates: CaCertificate[], t: any, linuxDistro?: string, setLinuxDistro?: (distro: any) => void }) {
|
|
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-10 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>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed font-medium">
|
|
{step}
|
|
</p>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
|
|
{/* Global Bundle Section (Recommendations) */}
|
|
<div className="pt-8 border-t border-dashed border-gray-100 dark:border-gray-700">
|
|
<div className="relative bg-brand-500/5 dark:bg-brand-400/5 rounded-2xl p-6 border border-brand-500/10 dark:border-brand-400/10">
|
|
<div className="absolute top-4 right-4">
|
|
<Badge variant="brand">{t('recommended')}</Badge>
|
|
</div>
|
|
<div className="flex flex-col sm:flex-row items-start gap-4">
|
|
<div className="w-12 h-12 rounded-2xl bg-gradient-to-br from-brand-500 to-brand-600 text-white flex items-center justify-center flex-shrink-0 shadow-lg shadow-brand-500/20">
|
|
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
</svg>
|
|
</div>
|
|
<div className="flex-1 pt-1">
|
|
<h5 className="font-bold text-lg text-gray-900 dark:text-white mb-2 pr-20">
|
|
{t('bundle_guide_title')}
|
|
</h5>
|
|
<p className="text-sm text-gray-600 dark:text-gray-400 leading-relaxed mb-6 max-w-xl">
|
|
{t('bundle_guide_desc')}
|
|
</p>
|
|
|
|
{(selectedOs === 'linux' || selectedOs === 'windows' || selectedOs === 'macos') && (
|
|
<div className="space-y-4">
|
|
{(selectedOs === 'linux') && (
|
|
<div className="space-y-4">
|
|
<div className="flex flex-wrap gap-2 mb-4">
|
|
<button onClick={() => setLinuxDistro?.('debian')} className={`px-3 py-1.5 rounded-lg text-xs font-bold border transition-colors ${linuxDistro === 'debian' ? 'bg-brand-50 border-brand-200 text-brand-700 dark:bg-brand-500/10 dark:border-brand-500/20 dark:text-brand-400' : 'bg-transparent border-transparent text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800'}`}>Debian/Ubuntu</button>
|
|
<button onClick={() => setLinuxDistro?.('rhel')} className={`px-3 py-1.5 rounded-lg text-xs font-bold border transition-colors ${linuxDistro === 'rhel' ? 'bg-brand-50 border-brand-200 text-brand-700 dark:bg-brand-500/10 dark:border-brand-500/20 dark:text-brand-400' : 'bg-transparent border-transparent text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800'}`}>CentOS/RHEL</button>
|
|
<button onClick={() => setLinuxDistro?.('arch')} className={`px-3 py-1.5 rounded-lg text-xs font-bold border transition-colors ${linuxDistro === 'arch' ? 'bg-brand-50 border-brand-200 text-brand-700 dark:bg-brand-500/10 dark:border-brand-500/20 dark:text-brand-400' : 'bg-transparent border-transparent text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800'}`}>Arch Linux</button>
|
|
<button onClick={() => setLinuxDistro?.('other')} className={`px-3 py-1.5 rounded-lg text-xs font-bold border transition-colors ${linuxDistro === 'other' ? 'bg-brand-50 border-brand-200 text-brand-700 dark:bg-brand-500/10 dark:border-brand-500/20 dark:text-brand-400' : 'bg-transparent border-transparent text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800'}`}>Other</button>
|
|
</div>
|
|
|
|
{linuxDistro === 'debian' && (
|
|
<CliSnippet
|
|
label={`${t('bundle_cli_label')} (Debian/Ubuntu)`}
|
|
command={`sudo apt update && sudo apt install -y curl && curl -sL https://cdn.trustlab.dyzulk.com/ca/bundles/trustlab-all.sh | sudo bash`}
|
|
t={t}
|
|
/>
|
|
)}
|
|
{linuxDistro === 'rhel' && (
|
|
<CliSnippet
|
|
label={`${t('bundle_cli_label')} (RHEL/CentOS)`}
|
|
command={`(sudo yum install -y curl || sudo dnf install -y curl) && curl -sL https://cdn.trustlab.dyzulk.com/ca/bundles/trustlab-all.sh | sudo bash`}
|
|
t={t}
|
|
/>
|
|
)}
|
|
{linuxDistro === 'arch' && (
|
|
<CliSnippet
|
|
label={`${t('bundle_cli_label')} (Arch Linux)`}
|
|
command={`sudo pacman -Sy --noconfirm curl && curl -sL https://cdn.trustlab.dyzulk.com/ca/bundles/trustlab-all.sh | sudo bash`}
|
|
t={t}
|
|
/>
|
|
)}
|
|
{linuxDistro === 'other' && (
|
|
<CliSnippet
|
|
label={`${t('bundle_cli_label')} (Universal)`}
|
|
command={`curl -sL https://cdn.trustlab.dyzulk.com/ca/bundles/trustlab-all.sh | sudo bash`}
|
|
t={t}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
{(selectedOs === 'windows' || selectedOs === 'macos') && (
|
|
<div className="flex flex-wrap gap-2">
|
|
<DownloadBtn
|
|
href={`https://cdn.trustlab.dyzulk.com/ca/bundles/trustlab-all.${selectedOs === 'windows' ? 'bat' : 'mobileconfig'}`}
|
|
label={t('download_all_bundle')}
|
|
icon={selectedOs === 'windows' ? <WindowsIcon className="w-4 h-4" /> : <AppleIcon className="w-4 h-4" />}
|
|
onClick={() => {}}
|
|
variant="blue"
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{selectedOs === 'linux' && (
|
|
<div className="pt-10 border-t border-gray-100 dark:border-gray-700 space-y-6">
|
|
<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>
|
|
|
|
{/* Distro Selection for Individual Certs */}
|
|
<div className="flex flex-wrap gap-2 mb-2">
|
|
<button onClick={() => setLinuxDistro?.('debian')} className={`px-2.5 py-1 rounded-md text-[10px] uppercase tracking-wider font-bold border transition-colors ${linuxDistro === 'debian' ? 'bg-brand-50 border-brand-200 text-brand-700 dark:bg-brand-500/10 dark:border-brand-500/20 dark:text-brand-400' : 'bg-transparent border-transparent text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'}`}>Debian</button>
|
|
<button onClick={() => setLinuxDistro?.('rhel')} className={`px-2.5 py-1 rounded-md text-[10px] uppercase tracking-wider font-bold border transition-colors ${linuxDistro === 'rhel' ? 'bg-brand-50 border-brand-200 text-brand-700 dark:bg-brand-500/10 dark:border-brand-500/20 dark:text-brand-400' : 'bg-transparent border-transparent text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'}`}>RHEL</button>
|
|
<button onClick={() => setLinuxDistro?.('arch')} className={`px-2.5 py-1 rounded-md text-[10px] uppercase tracking-wider font-bold border transition-colors ${linuxDistro === 'arch' ? 'bg-brand-50 border-brand-200 text-brand-700 dark:bg-brand-500/10 dark:border-brand-500/20 dark:text-brand-400' : 'bg-transparent border-transparent text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'}`}>Arch</button>
|
|
<button onClick={() => setLinuxDistro?.('other')} className={`px-2.5 py-1 rounded-md text-[10px] uppercase tracking-wider font-bold border transition-colors ${linuxDistro === 'other' ? 'bg-brand-50 border-brand-200 text-brand-700 dark:bg-brand-500/10 dark:border-brand-500/20 dark:text-brand-400' : 'bg-transparent border-transparent text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'}`}>Universal</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-6">
|
|
{/* Function to generate smart command */}
|
|
{(() => {
|
|
const getSmartCommand = (url: string | null) => {
|
|
if (!url) return '';
|
|
if (linuxDistro === 'debian') return `sudo apt install -y curl && curl -sL ${url} | sudo bash`;
|
|
if (linuxDistro === 'rhel') return `(sudo yum install -y curl || sudo dnf install -y curl) && curl -sL ${url} | sudo bash`;
|
|
if (linuxDistro === 'arch') return `sudo pacman -Sy curl && curl -sL ${url} | sudo bash`;
|
|
return `curl -sL ${url} | sudo bash`;
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{/* Root CAs */}
|
|
{certificates.filter(c => c.type === 'root').map(c => (
|
|
<CliSnippet
|
|
key={c.serial}
|
|
label={`${t('root_ca')}: ${c.name} [${linuxDistro?.toUpperCase()}]`}
|
|
command={getSmartCommand(c.linux_cdn_url || null)}
|
|
t={t}
|
|
/>
|
|
))}
|
|
|
|
{/* Intermediate CAs */}
|
|
{certificates.filter(c => c.type !== 'root').map(c => (
|
|
<CliSnippet
|
|
key={c.serial}
|
|
label={`${t('intermediate_ca')}: ${c.name} [${linuxDistro?.toUpperCase()}]`}
|
|
command={getSmartCommand(c.linux_cdn_url || null)}
|
|
t={t}
|
|
/>
|
|
))}
|
|
</>
|
|
);
|
|
})()}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function CaCard({ cert, isRoot, t, selectedOs, setSelectedOs }: { cert: CaCertificate, isRoot: boolean, t: any, selectedOs: string, setSelectedOs: (os: any) => void }) {
|
|
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 */}
|
|
{cert.last_synced_at && (
|
|
<div className="absolute top-6 right-8 flex items-center gap-1.5 px-2 py-1 rounded-lg bg-green-500/10 border border-green-500/20 text-[10px] font-bold text-green-600 dark:text-green-400 uppercase tracking-tighter">
|
|
<span className="relative flex h-2 w-2">
|
|
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
|
|
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
|
|
</span>
|
|
{t('synced_to_cdn')}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6 mb-8">
|
|
<div className="flex items-center gap-6">
|
|
<div className={`w-16 h-16 rounded-3xl flex items-center justify-center shadow-lg transition-transform group-hover:scale-110 duration-500 ${
|
|
isRoot ? 'bg-brand-500 text-white shadow-brand-500/20' : 'bg-blue-500 text-white shadow-blue-500/20'
|
|
}`}>
|
|
<svg className="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<Badge variant={isRoot ? 'purple' : 'blue'}>
|
|
{isRoot ? t('root_ca') : t('intermediate_ca')}
|
|
</Badge>
|
|
<h4 className="text-2xl font-black text-gray-900 dark:text-white mt-1 group-hover:text-brand-500 transition-colors">{cert.name}</h4>
|
|
<div className="flex items-center gap-4 mt-2 font-mono text-[10px] text-gray-400">
|
|
<span className="flex items-center gap-1.5">
|
|
<span className="w-1 h-1 rounded-full bg-gray-400"></span>
|
|
ID: {cert.serial}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
|
|
<DownloadBtn
|
|
href={cert.cdn_url || `/api/public/ca-certificates/${cert.serial}/download`}
|
|
label={t('download_standard') || "PEM"}
|
|
icon={<FileIcon className="w-4 h-4" />}
|
|
onClick={() => {}}
|
|
/>
|
|
<DownloadBtn
|
|
href={cert.der_cdn_url || `/api/public/ca-certificates/${cert.serial}/download?format=der`}
|
|
label={t('download_android') || "Android/DER"}
|
|
icon={<MobileIcon className="w-4 h-4" />}
|
|
onClick={() => setSelectedOs('mobile')}
|
|
variant="green"
|
|
/>
|
|
<DownloadBtn
|
|
href={cert.bat_cdn_url || `/api/public/ca-certificates/${cert.serial}/download/windows`}
|
|
label={t('download_windows') || "Windows"}
|
|
icon={<WindowsIcon className="w-4 h-4" />}
|
|
onClick={() => setSelectedOs('windows')}
|
|
variant="blue"
|
|
/>
|
|
<DownloadBtn
|
|
href={cert.mac_cdn_url || `/api/public/ca-certificates/${cert.serial}/download/mac`}
|
|
label={t('download_macos') || "macOS"}
|
|
icon={<AppleIcon className="w-4 h-4" />}
|
|
onClick={() => setSelectedOs('macos')}
|
|
variant="gray"
|
|
/>
|
|
</div>
|
|
|
|
{cert.linux_cdn_url && (
|
|
<div className="mt-4">
|
|
<DownloadBtn
|
|
href={cert.linux_cdn_url || `/api/public/ca-certificates/${cert.serial}/download/linux`}
|
|
label={t('download_linux') || "Linux (.sh)"}
|
|
icon={<LinuxIcon className="w-4 h-4" />}
|
|
onClick={() => setSelectedOs('linux')}
|
|
variant="gray"
|
|
isFullWidth
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function DownloadBtn({ href, label, icon, onClick, variant = 'white', isFullWidth = false }: { href: string, label: string, icon: React.ReactNode, onClick: () => void, variant?: 'white' | 'green' | 'blue' | 'gray', isFullWidth?: boolean }) {
|
|
const variants = {
|
|
white: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700',
|
|
green: 'bg-green-50 dark:bg-green-500/10 text-green-700 dark:text-green-400 border-green-200 dark:border-green-500/20 hover:bg-green-100 dark:hover:bg-green-500/20',
|
|
blue: 'bg-blue-50 dark:bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-200 dark:border-blue-500/20 hover:bg-blue-100 dark:hover:bg-blue-500/20',
|
|
gray: 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-100 border-gray-200 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600',
|
|
};
|
|
|
|
return (
|
|
<a
|
|
href={href}
|
|
onClick={onClick}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className={`flex items-center justify-center gap-2 px-4 py-3 rounded-2xl border font-bold text-xs transition-all duration-300 hover:shadow-lg active:scale-95 ${variants[variant]} ${isFullWidth ? 'w-full' : ''}`}
|
|
>
|
|
{icon}
|
|
{label}
|
|
</a>
|
|
);
|
|
}
|
|
|
|
// Icons
|
|
const WindowsIcon = ({ className }: { className?: string }) => (
|
|
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M0 3.449L9.75 2.134V11.3L0 11.3V3.449ZM9.75 12.7L0 12.7V20.551L9.75 19.166V12.7ZM10.5 1.998L24 0.166V11.3L10.5 11.3V1.998ZM10.5 12.7L24 12.7V23.834L10.5 21.966V12.7Z"/>
|
|
</svg>
|
|
);
|
|
|
|
const AppleIcon = ({ className }: { className?: string }) => (
|
|
<svg className={className} 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.21-1.96 1.07-3.11-1.05.05-2.31.74-3.03 1.59-.65.77-1.2 2.02-1.07 3.12 1.17.09 2.36-.73 3.03-1.6"/>
|
|
</svg>
|
|
);
|
|
|
|
const LinuxIcon = ({ className }: { className?: string }) => (
|
|
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M11.97 2c-3.14 0-5.69 2.51-5.69 5.61v.24c-.11-.03-.23-.05-.36-.05-1.16 0-2.11.93-2.11 2.07s.95 2.06 2.11 2.06c.13 0 .25-.01.36-.04v.24c0 3.1 2.55 5.61 5.69 5.61 3.14 0 5.69-2.51 5.69-5.61v-.24c.11.03.22.04.35.04 1.16 0 2.11-.93 2.11-2.06s-.95-2.07-2.11-2.07c-.13 0-.24.02-.35.05v-.24C17.66 4.51 15.11 2 11.97 2zm0 1.87c2.1 0 3.8 1.67 3.8 3.74v.17c-.38-.08-.78-.13-1.2-.13-2.13 0-3.86 1.69-3.86 3.78s1.73 3.78 3.86 3.78c.42 0 .82-.05 1.2-.13v.17c0 2.07-1.7 3.74-3.8 3.74-2.1 0-3.8-1.67-3.8-3.74v-.17c.38.08.78.13 1.2.13 2.13 0 3.86-1.69 3.86-3.78s-1.73-3.78-3.86-3.78c-.42 0-.82.05-1.2.13v-.17c0-2.07 1.7-3.74 3.8-3.74zM8.33 9.4c.58 0 1.05.47 1.05 1.04s-.47 1.04-1.05 1.04-1.05-.47-1.05-1.04.47-1.04 1.05-1.04zm7.28 0c.58 0 1.05.47 1.05 1.04s-.47 1.04-1.05 1.04-1.05-.47-1.05-1.04.47-1.04 1.05-1.04zM12 21c-1.1 0-2 .9-2 2h4c0-1.1-.9-2-2-2z"/>
|
|
</svg>
|
|
);
|
|
|
|
const MobileIcon = ({ className }: { className?: string }) => (
|
|
<svg className={className} viewBox="0 0 24 24" fill="currentColor">
|
|
<path d="M17 2H7c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 18H7V4h10v16z"/>
|
|
</svg>
|
|
);
|
|
|
|
const FileIcon = ({ className }: { className?: string }) => (
|
|
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
|
</svg>
|
|
);
|
|
|
|
const CopyIcon = ({ className }: { className?: string }) => (
|
|
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
|
|
</svg>
|
|
);
|
|
|
|
const CheckIcon = ({ className }: { className?: string }) => (
|
|
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
|
|
</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[]>([]);
|
|
const [loadingCerts, setLoadingCerts] = useState(true);
|
|
const [selectedOs, setSelectedOs] = useState<'windows' | 'macos' | 'linux' | 'mobile'>('windows');
|
|
const [linuxDistro, setLinuxDistro] = useState<'debian' | 'rhel' | 'arch' | 'other'>('debian');
|
|
|
|
useEffect(() => {
|
|
const fetchCertificates = async () => {
|
|
try {
|
|
const response = await axios.get('/api/public/ca-certificates');
|
|
if (response.data.success) {
|
|
setCertificates(response.data.data);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to fetch certificates", error);
|
|
} finally {
|
|
setLoadingCerts(false);
|
|
}
|
|
};
|
|
|
|
fetchCertificates();
|
|
}, []);
|
|
|
|
const handleScroll = (e: React.MouseEvent<HTMLAnchorElement>, id: string) => {
|
|
e.preventDefault();
|
|
const element = document.querySelector(id);
|
|
if (element) {
|
|
const navbarOffset = 80;
|
|
const elementPosition = element.getBoundingClientRect().top;
|
|
const offsetPosition = elementPosition + window.pageYOffset - navbarOffset;
|
|
window.scrollTo({
|
|
top: offsetPosition,
|
|
behavior: "smooth",
|
|
});
|
|
}
|
|
};
|
|
|
|
|
|
return (
|
|
<div className="relative flex-grow flex flex-col">
|
|
{/* Hero Section */}
|
|
<header className="relative pt-32 pb-20 overflow-hidden" id="home">
|
|
{/* Background Shapes */}
|
|
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-full max-w-7xl h-full -z-10 opacity-30 dark:opacity-20 pointer-events-none">
|
|
<div className="absolute top-20 left-10 w-72 h-72 bg-brand-500 rounded-full blur-[120px]"></div>
|
|
<div className="absolute bottom-10 right-10 w-96 h-96 bg-blue-500 rounded-full blur-[150px]"></div>
|
|
</div>
|
|
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
|
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-brand-50 dark:bg-brand-500/10 border border-brand-100 dark:border-brand-500/20 text-brand-600 dark:text-brand-400 text-xs font-bold uppercase tracking-widest mb-8 animate-bounce">
|
|
🚀 {t('hero_tag')}
|
|
</div>
|
|
<h1 className="text-5xl md:text-7xl font-extrabold text-gray-900 dark:text-white mb-6 leading-tight">
|
|
{t('hero_title_1')} <br/>
|
|
<span className="text-transparent bg-clip-text bg-gradient-to-r from-brand-500 to-blue-600">
|
|
{t('hero_title_2')}
|
|
</span>
|
|
</h1>
|
|
<p className="text-lg md:text-xl text-gray-600 dark:text-gray-400 max-w-2xl mx-auto mb-10">
|
|
{t('hero_desc')}
|
|
</p>
|
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
|
<Link href="/signup" className="w-full sm:w-auto px-8 py-4 bg-gray-900 dark:bg-white text-white dark:text-gray-900 rounded-2xl font-bold shadow-xl transition-all hover:-translate-y-1 hover:shadow-2xl">
|
|
{t('cta_create_account')}
|
|
</Link>
|
|
<a href="#features" onClick={(e) => handleScroll(e, '#features')} className="w-full sm:w-auto px-8 py-4 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-200 border border-gray-200 dark:border-gray-700 rounded-2xl font-bold transition-all hover:bg-gray-50 dark:hover:bg-gray-700 cursor-pointer">
|
|
{t('cta_explore_features')}
|
|
</a>
|
|
</div>
|
|
|
|
{/* Preview/Abstract UI */}
|
|
<div className="mt-20 relative mx-auto max-w-5xl">
|
|
<div className="aspect-video bg-white dark:bg-gray-800 rounded-3xl border border-gray-200 dark:border-gray-700 shadow-2xl p-4 overflow-hidden group">
|
|
<div className="flex items-center gap-2 mb-4 border-b border-gray-100 dark:border-gray-700 pb-3">
|
|
<div className="flex gap-1.5">
|
|
<div className="w-3 h-3 rounded-full bg-red-400"></div>
|
|
<div className="w-3 h-3 rounded-full bg-yellow-400"></div>
|
|
<div className="w-3 h-3 rounded-full bg-green-400"></div>
|
|
</div>
|
|
<div className="flex-1 ml-4 h-6 bg-gray-100 dark:bg-gray-900/50 rounded-lg max-w-xs"></div>
|
|
</div>
|
|
{/* Mock Dashboard Content */}
|
|
<div className="grid grid-cols-3 gap-4">
|
|
<div className="col-span-2 space-y-4">
|
|
<div className="h-40 bg-brand-500/5 rounded-2xl border border-brand-500/10"></div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div className="h-24 bg-gray-50 dark:bg-gray-900/50 rounded-2xl"></div>
|
|
<div className="h-24 bg-gray-50 dark:bg-gray-900/50 rounded-2xl"></div>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-4">
|
|
<div className="h-full bg-gray-50 dark:bg-gray-900/50 rounded-2xl"></div>
|
|
</div>
|
|
</div>
|
|
{/* Overlay Gradient */}
|
|
<div className="absolute inset-0 bg-gradient-to-t from-white dark:from-gray-900 via-transparent to-transparent pointer-events-none"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
{/* Features Section */}
|
|
<section id="features" className="py-24 bg-gray-50 dark:bg-gray-900/50">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="text-center mb-16">
|
|
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">{t('features_title')}</h2>
|
|
<p className="text-gray-600 dark:text-gray-400">{t('features_desc')}</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
|
{/* Feature 1 */}
|
|
<div className="bg-white dark:bg-gray-800 p-8 rounded-3xl border border-gray-100 dark:border-gray-700 shadow-sm hover:shadow-xl transition-all duration-300 group">
|
|
<div className="w-14 h-14 bg-brand-50 dark:bg-brand-500/10 rounded-2xl flex items-center justify-center text-brand-500 mb-6 group-hover:scale-110 transition-transform">
|
|
<svg className="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-3">{t('feature_1_title')}</h3>
|
|
<p className="text-gray-600 dark:text-gray-400 text-sm leading-relaxed">
|
|
{t('feature_1_desc')}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Feature 2 */}
|
|
<div className="bg-white dark:bg-gray-800 p-8 rounded-3xl border border-gray-100 dark:border-gray-700 shadow-sm hover:shadow-xl transition-all duration-300 group">
|
|
<div className="w-14 h-14 bg-blue-50 dark:bg-blue-500/10 rounded-2xl flex items-center justify-center text-blue-500 mb-6 group-hover:scale-110 transition-transform">
|
|
<svg className="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11.536 11 9 13.536 7.464 12 4.929 14.536V17h2.472l4.243-4.243a6 6 0 018.828-5.743zM16.5 13.5V18h6v-4.5h-6z" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-3">{t('feature_2_title')}</h3>
|
|
<p className="text-gray-600 dark:text-gray-400 text-sm leading-relaxed">
|
|
{t('feature_2_desc')}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Feature 3 */}
|
|
<div className="bg-white dark:bg-gray-800 p-8 rounded-3xl border border-gray-100 dark:border-gray-700 shadow-sm hover:shadow-xl transition-all duration-300 group">
|
|
<div className="w-14 h-14 bg-green-50 dark:bg-green-500/10 rounded-2xl flex items-center justify-center text-green-500 mb-6 group-hover:scale-110 transition-transform">
|
|
<svg className="h-7 w-7" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
</svg>
|
|
</div>
|
|
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-3">{t('feature_3_title')}</h3>
|
|
<p className="text-gray-600 dark:text-gray-400 text-sm leading-relaxed">
|
|
{t('feature_3_desc')}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Trust Store Section */}
|
|
<section id="trust-store" className="py-24 bg-white dark:bg-gray-800 relative overflow-hidden">
|
|
{/* Gradient Background */}
|
|
<div className="absolute inset-0 opacity-30 dark:opacity-10 pointer-events-none">
|
|
<div className="absolute -top-20 -right-20 w-96 h-96 bg-brand-500/20 rounded-full blur-[100px]"></div>
|
|
<div className="absolute -bottom-20 -left-20 w-80 h-80 bg-blue-500/20 rounded-full blur-[100px]"></div>
|
|
</div>
|
|
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
|
<div className="text-center mb-16">
|
|
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 dark:text-white mb-4">{t('trust_store_title')}</h2>
|
|
<p className="text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
|
|
{t('trust_store_desc')}
|
|
</p>
|
|
</div>
|
|
|
|
{loadingCerts ? (
|
|
<div className="flex justify-center py-12">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-brand-500"></div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-24">
|
|
{Object.entries(
|
|
certificates.reduce((acc, cert) => {
|
|
const famId = cert.family_id || 'unknown';
|
|
if (!acc[famId]) acc[famId] = [];
|
|
acc[famId].push(cert);
|
|
return acc;
|
|
}, {} as Record<string, CaCertificate[]>)
|
|
).map(([famId, familyCerts]) => (
|
|
<div key={famId} className="flex flex-col items-center">
|
|
{/* Root Section of the Family */}
|
|
<div className="w-full flex flex-col items-center">
|
|
<h3 className="text-sm font-bold text-brand-500 uppercase tracking-widest mb-8 px-4 py-1 bg-brand-50 dark:bg-brand-500/10 rounded-full border border-brand-100 dark:border-brand-500/20">
|
|
{t('root_ca_hierarchy')}
|
|
</h3>
|
|
<div className="grid grid-cols-1 gap-8 w-full max-w-3xl px-4">
|
|
{familyCerts.filter(c => c.type === 'root').map((cert) => (
|
|
<CaCard key={cert.serial} cert={cert} isRoot={true} t={t} selectedOs={selectedOs} setSelectedOs={setSelectedOs} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Connecting Line */}
|
|
<div className="w-px h-16 bg-gradient-to-b from-brand-500/50 via-blue-500/30 to-transparent my-4"></div>
|
|
|
|
{/* Intermediates Section of the Family */}
|
|
<div className="w-full flex flex-col items-center">
|
|
<h3 className="text-sm font-bold text-blue-500 uppercase tracking-widest mb-8 px-4 py-1 bg-blue-50 dark:bg-blue-500/10 rounded-full border border-blue-100 dark:border-blue-500/20">
|
|
{t('intermediate_ca_hierarchy')}
|
|
</h3>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full max-w-6xl px-4">
|
|
{familyCerts.filter(c => c.type !== 'root').map((cert) => (
|
|
<CaCard key={cert.serial} cert={cert} isRoot={false} t={t} selectedOs={selectedOs} setSelectedOs={setSelectedOs} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{/* OS Selection Tabs for Global Guide */}
|
|
<div className="max-w-4xl mx-auto mt-20 p-8 rounded-[2rem] bg-gray-50 dark:bg-gray-900/50 border border-gray-100 dark:border-gray-800">
|
|
<div className="text-center mb-8">
|
|
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-2">{t('install_guide_title')}</h3>
|
|
<p className="text-gray-600 dark:text-gray-400">{t('install_guide_desc')}</p>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap justify-center gap-2 mb-8 p-1.5 bg-gray-200/50 dark:bg-gray-800/50 rounded-2xl">
|
|
<TabButton id="windows" label="Windows" active={selectedOs === 'windows'} onClick={() => setSelectedOs('windows')} icon={<WindowsIcon className="w-4 h-4" />} />
|
|
<TabButton id="macos" label="macOS" active={selectedOs === 'macos'} onClick={() => setSelectedOs('macos')} icon={<AppleIcon className="w-4 h-4" />} />
|
|
<TabButton id="linux" label="Linux" active={selectedOs === 'linux'} onClick={() => setSelectedOs('linux')} icon={<LinuxIcon className="w-4 h-4" />} />
|
|
<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">
|
|
{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} linuxDistro={linuxDistro} setLinuxDistro={setLinuxDistro} />}
|
|
{selectedOs === 'mobile' && <OsGuideContent title="Mobile Installation" steps={t.raw('guide_steps_mobile')} selectedOs={selectedOs} certificates={certificates} t={t} />}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
|
|
{/* CTA Section */}
|
|
<section className="py-20">
|
|
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div className="bg-brand-600 rounded-[3rem] p-12 md:p-16 text-center text-white relative overflow-hidden shadow-2xl">
|
|
<div className="relative z-10">
|
|
<h2 className="text-3xl md:text-4xl font-bold mb-6">{t('cta_ready_title')}</h2>
|
|
<p className="text-brand-100 mb-10 max-w-lg mx-auto">{t('cta_ready_desc')}</p>
|
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
|
<Link href="/signup" className="px-8 py-4 bg-white text-brand-600 rounded-2xl font-bold hover:scale-105 transition-transform">
|
|
{t('cta_free_account')}
|
|
</Link>
|
|
<Link href="/signin" className="px-8 py-4 bg-brand-700 text-white rounded-2xl font-bold hover:bg-brand-800 transition-colors">
|
|
{t('cta_signin_portal')}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
{/* Abstract Design */}
|
|
<div className="absolute top-0 left-0 w-full h-full opacity-10 pointer-events-none">
|
|
<CommonGridShape />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Back to Top Button */}
|
|
<ScrollToTop />
|
|
</div>
|
|
);
|
|
}
|