Files
Domain_web/web/connect-app.jsx
Андрей Бобырев e5ea14f046 feat(deploy): full one-line install with search API
Bundle static site, FastAPI search, nginx proxy, and systemd unit so
a fresh Ubuntu VPS can run curl install.sh and get /search working.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-29 22:29:10 +03:00

524 lines
26 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// VPN connection guide — Apple-light style
const { useState: useStateC, useEffect: useEffectC } = React;
// SiteNav from nav.jsx
// ───────── Hero ─────────
function Hero(){
return (
<section style={{maxWidth:760, margin:"0 auto", padding:"56px 28px 28px", textAlign:"center"}}>
<div style={{display:"inline-flex", alignItems:"center", gap:6,
padding:"6px 14px", borderRadius:999, background:"#fff",
border:"1px solid var(--line)", boxShadow:"0 1px 1px rgba(16,24,40,.03)",
fontSize:11, fontWeight:700, color:"var(--accent-2)", letterSpacing:".12em"}}>
<span style={{width:6, height:6, borderRadius:999, background:"var(--accent)"}}/>
ЧАСТНЫЙ СЕРВЕР
</div>
<h1 style={{margin:"18px 0 14px", fontSize:54, fontWeight:800, letterSpacing:"-0.035em", lineHeight:1.04}}>
Подключение к VPN
</h1>
<p style={{margin:"0 auto", maxWidth:560, fontSize:16, lineHeight:1.55, color:"var(--ink-2)"}}>
Используйте приложение <strong>AmneziaVPN</strong>. Конфигурацию выдаёт администратор
импортируйте файл или ключ, который вам передали, и включите туннель. Ниже пошаговые
инструкции для WireGuard и Amnezia VPN.
</p>
</section>
);
}
const DEFAULT_SERVER_IP = "SERVER_IP";
// ───────── Server info card ─────────
function ServerCard(){
const [copied,setCopied] = useStateC(false);
const [copiedProxy,setCopiedProxy] = useStateC(false);
const [config,setConfig] = useStateC({
server_ip: DEFAULT_SERVER_IP,
telegram_proxy: "",
telegram_proxy_label: "Прокси Telegram",
});
useEffectC(() => {
fetch("/data/site-config.json?t=" + Date.now())
.then(r => r.ok ? r.json() : null)
.then(data => { if (data) setConfig(prev => ({ ...prev, ...data })); })
.catch(() => {});
}, []);
const serverIp = (config.server_ip || DEFAULT_SERVER_IP).trim();
const proxyLabel = config.telegram_proxy_label || "Прокси Telegram";
const proxyUrl = (config.telegram_proxy || "").trim();
function copyIp(){
navigator.clipboard?.writeText(serverIp);
setCopied(true);
setTimeout(()=>setCopied(false), 1600);
}
function copyProxy(){
if (!proxyUrl) return;
navigator.clipboard?.writeText(proxyUrl);
setCopiedProxy(true);
setTimeout(()=>setCopiedProxy(false), 1600);
}
return (
<Card padding={24} style={{maxWidth:880, margin:"0 auto 18px"}}>
<div style={{fontSize:11, fontWeight:700, letterSpacing:".14em", color:"var(--accent-2)",
textTransform:"uppercase"}}>Сервер</div>
<div style={{marginTop:10, fontSize:13, fontWeight:500, color:"var(--ink-3)"}}>Адрес</div>
<div style={{marginTop:8, display:"flex", gap:10}}>
<div style={{flex:1, height:54, display:"flex", alignItems:"center", padding:"0 18px",
background:"var(--bg-tint)", border:"1px solid var(--line)", borderRadius:14,
fontFamily:"var(--mono)", fontSize:18, fontWeight:600,
letterSpacing:"-0.005em", color:"var(--ink)"}}>
{serverIp}
</div>
<Button variant={copied?"success":"primary"} size="lg" onClick={copyIp}
icon={copied?"check":"clipboard"}>{copied?"Скопировано":"Копировать"}</Button>
</div>
<div style={{marginTop:18, paddingTop:18, borderTop:"1px solid var(--line-2)"}}>
<div style={{fontSize:13, fontWeight:500, color:"var(--ink-3)"}}>{proxyLabel}</div>
{proxyUrl ? (
<div style={{marginTop:8, display:"flex", gap:10}}>
<div style={{flex:1, minHeight:54, display:"flex", alignItems:"center", padding:"12px 18px",
background:"var(--bg-tint)", border:"1px solid var(--line)", borderRadius:14,
fontFamily:"var(--mono)", fontSize:14, fontWeight:500,
letterSpacing:"-0.005em", color:"var(--ink)", wordBreak:"break-all"}}>
<a href={proxyUrl} target="_blank" rel="noreferrer"
style={{color:"var(--accent-2)", fontWeight:600, textDecoration:"underline",
textUnderlineOffset:3}}>
{proxyUrl}
</a>
</div>
<Button variant={copiedProxy?"success":"primary"} size="lg" onClick={copyProxy}
icon={copiedProxy?"check":"clipboard"}>{copiedProxy?"Скопировано":"Копировать"}</Button>
</div>
) : (
<div style={{marginTop:8, padding:"14px 18px", background:"var(--bg-tint)",
border:"1px dashed var(--line)", borderRadius:14}}>
<div style={{fontSize:15, fontWeight:600, color:"var(--ink-2)"}}>Не настроено</div>
<p style={{margin:"8px 0 0", fontSize:12.5, lineHeight:1.5, color:"var(--ink-3)"}}>
Администратор может добавить ссылку в{" "}
<code style={{fontSize:12, background:"#fff", padding:"2px 6px", borderRadius:6,
border:"1px solid var(--line-2)"}}>/var/www/domain-web/data/site-config.json</code>
{" "} поле <code style={{fontSize:12, background:"#fff", padding:"2px 6px",
borderRadius:6, border:"1px solid var(--line-2)"}}>telegram_proxy</code>.
Подробнее в <code style={{fontSize:12, background:"#fff", padding:"2px 6px",
borderRadius:6, border:"1px solid var(--line-2)"}}>data/README.txt</code> на сервере.
</p>
</div>
)}
</div>
<div style={{marginTop:18, paddingTop:18, borderTop:"1px solid var(--line-2)",
display:"flex", alignItems:"center", gap:14, flexWrap:"wrap"}}>
<span style={{fontSize:13.5, color:"var(--ink-2)", fontWeight:500}}>Сервисы на машине:</span>
<Badge tone="blue" dot>XRay (TCP)</Badge>
<Badge tone="purple" dot>AmneziaWG (UDP)</Badge>
</div>
<p style={{margin:"12px 0 0", fontSize:12.5, color:"var(--ink-3)"}}>
Номера портов и тип протокола уже зашиты в конфиг Amnezia вручную их обычно не вводят.
</p>
</Card>
);
}
// ───────── Big segmented control ─────────
function BigSegment({ value, options, onChange }){
return (
<div style={{display:"grid", gridTemplateColumns:`repeat(${options.length},1fr)`,
gap:8, padding:6, background:"#fff", border:"1px solid var(--line)", borderRadius:18,
boxShadow:"0 1px 1px rgba(16,24,40,.03)"}}>
{options.map(o=>{
const on = value===o.id;
return (
<button key={o.id} onClick={()=>onChange(o.id)} style={{
appearance:"none", border:0, height:52, padding:"0 16px",
background: on ? "var(--accent-soft)" : "transparent",
color: on ? "var(--accent-2)" : "var(--ink-2)",
borderRadius:13, cursor:"pointer", fontWeight: on ? 600 : 500,
fontSize:14.5, letterSpacing:"-0.005em",
display:"inline-flex", alignItems:"center", justifyContent:"center", gap:10,
transition:"all .15s ease",
}}>
{o.label}
{o.tag && <Badge tone={o.tagTone || "neutral"} size="sm">{o.tag}</Badge>}
</button>
);
})}
</div>
);
}
// ───────── OS tabs (smaller, pill) ─────────
function OSTabs({ value, onChange }){
const os = [
{id:"ios", label:"iOS"}, {id:"android", label:"Android"},
{id:"windows", label:"Windows"}, {id:"macos", label:"macOS"}
];
return (
<div style={{display:"grid", gridTemplateColumns:"repeat(4,1fr)", gap:6,
padding:5, background:"#fff", border:"1px solid var(--line)", borderRadius:14,
boxShadow:"0 1px 1px rgba(16,24,40,.03)"}}>
{os.map(o=>{
const on = value===o.id;
return (
<button key={o.id} onClick={()=>onChange(o.id)} style={{
appearance:"none", border:0, height:40,
background: on ? "var(--ink)" : "transparent",
color: on ? "#fff" : "var(--ink-2)",
borderRadius:10, cursor:"pointer", fontWeight: on ? 600 : 500,
fontSize:13.5, transition:"all .15s ease",
boxShadow: on ? "0 2px 8px rgba(0,0,0,.18)" : "none"
}}>{o.label}</button>
);
})}
</div>
);
}
// ───────── Numbered step ─────────
function Step({ n, children }){
return (
<div style={{display:"flex", alignItems:"flex-start", gap:16, padding:"18px 22px",
borderBottom:"1px solid var(--line-2)"}}>
<div style={{
flexShrink:0, width:32, height:32, borderRadius:999,
background:"var(--accent-soft)", color:"var(--accent-2)",
display:"grid", placeItems:"center", fontWeight:700, fontSize:14.5,
fontVariantNumeric:"tabular-nums"
}}>{n}</div>
<div style={{flex:1, fontSize:15.5, lineHeight:1.55, color:"var(--ink)", paddingTop:4}}>
{children}
</div>
</div>
);
}
function SuccessStep({ children }){
return (
<div style={{display:"flex", alignItems:"center", gap:14, padding:"18px 22px",
background:"var(--green-soft)", borderTop:"1px solid var(--line-2)"}}>
<div style={{
flexShrink:0, width:30, height:30, borderRadius:999,
background:"var(--green)", color:"#fff",
display:"grid", placeItems:"center",
boxShadow:"0 4px 10px rgba(52,199,89,.35)"
}}>
<Icon name="check" size={16} stroke={3} color="#fff"/>
</div>
<div style={{fontSize:15, fontWeight:500, color:"#1f7a3a"}}>{children}</div>
</div>
);
}
// ───────── Steps card ─────────
function StepsCard({ protocol, os }){
// Headlines per protocol / OS
const titles = {
wg:{
ios:{name:"WireGuard на iOS", sub:"iPhone и iPad · iOS 15 и новее", color:"linear-gradient(160deg,#5ac8fa,#0a84ff)", icon:"lock"},
android:{name:"WireGuard на Android", sub:"Android 8 и новее", color:"linear-gradient(160deg,#7ce0a2,#2cb67d)", icon:"lock"},
windows:{name:"WireGuard на Windows", sub:"Windows 10 / 11", color:"linear-gradient(160deg,#7ec2ff,#0a84ff)", icon:"lock"},
macos:{name:"WireGuard на macOS", sub:"macOS 12 и новее", color:"linear-gradient(160deg,#bdbdc0,#6e6e73)", icon:"lock"},
},
awg:{
ios:{name:"Amnezia VPN на iOS", sub:"AmneziaWG · обход DPI", color:"linear-gradient(160deg,#a385ff,#6a44f5)", icon:"shield"},
android:{name:"Amnezia VPN на Android", sub:"AmneziaWG · обход DPI", color:"linear-gradient(160deg,#9b82ff,#6a44f5)", icon:"shield"},
windows:{name:"Amnezia VPN на Windows", sub:"AmneziaWG · обход DPI", color:"linear-gradient(160deg,#a385ff,#6a44f5)", icon:"shield"},
macos:{name:"Amnezia VPN на macOS", sub:"AmneziaWG · обход DPI", color:"linear-gradient(160deg,#9b82ff,#6a44f5)", icon:"shield"},
},
};
const t = titles[protocol][os];
// Steps — protocol/os specific
let steps;
if (protocol==="wg"){
if (os==="ios") steps = [
<>Откройте <strong>App Store</strong>, найдите <strong>WireGuard</strong> и нажмите «Установить»</>,
<>Откройте приложение, нажмите <strong>+</strong> в правом верхнем углу</>,
<>
Выберите <strong>«Сканировать QR-код»</strong> и наведите камеру на QR конфиг добавится автоматически
<FineNote>Или «Создать из файла» выберите файл <code>.conf</code></FineNote>
</>,
<>Придумайте имя туннелю и нажмите <strong>«Сохранить»</strong></>,
<>Нажмите тумблер появится запрос, нажмите <strong>«Разрешить»</strong></>,
];
else if (os==="android") steps = [
<>Установите <strong>WireGuard</strong> из Google Play или скачайте <code>.apk</code> с wireguard.com</>,
<>Откройте приложение, тапните по <strong>«+»</strong> внизу справа</>,
<>
Выберите <strong>«Сканировать из QR-кода»</strong> и наведите камеру на QR
<FineNote>Или «Импорт из файла» найдите <code>.conf</code> в загрузках</FineNote>
</>,
<>Дайте имя туннелю и подтвердите создание</>,
<>Переключите тумблер вправо разрешите создание VPN-подключения</>,
];
else if (os==="windows") steps = [
<>Скачайте <strong>WireGuard for Windows</strong> с <code>wireguard.com/install</code></>,
<>Установите и откройте приложение</>,
<>Нажмите <strong>«Импорт туннеля из файла»</strong> и выберите <code>.conf</code></>,
<>Подтвердите создание туннеля</>,
<>Нажмите <strong>«Подключить»</strong> — индикатор станет зелёным</>,
];
else steps = [
<>Установите <strong>WireGuard</strong> из Mac App Store</>,
<>Откройте приложение в строке меню</>,
<>Выберите <strong>«Импорт туннеля из файла»</strong> <code>.conf</code></>,
<>Подтвердите добавление профиля в Системные настройки</>,
<>Включите тумблер и подтвердите системный запрос</>,
];
} else {
// Amnezia VPN (same flow across OS — slight wording variations)
steps = [
<>Скачайте приложение <strong>AmneziaVPN</strong> с <code>amnezia.org</code> или из стора своей платформы</>,
<>Запустите приложение и согласитесь с условиями использования</>,
<>
Нажмите <strong>«Добавить конфигурацию»</strong> <strong>«Сканировать QR-код»</strong>
<FineNote>Или импорт ключа: вставьте строку вида <code>vpn://…</code></FineNote>
</>,
<>При запросе системы разрешите создание VPN-профиля</>,
<>Нажмите большую кнопку <strong>«Подключиться»</strong> — индикатор станет зелёным</>,
];
}
return (
<Card padding={0} style={{overflow:"hidden"}}>
<div style={{display:"flex", alignItems:"center", gap:14, padding:"22px 22px 18px"}}>
<GlyphTile size={48} radius={14} gradient={t.color} icon={t.icon}/>
<div>
<h2 style={{margin:0, fontSize:20, fontWeight:700, letterSpacing:"-0.015em"}}>{t.name}</h2>
<div style={{marginTop:3, fontSize:13.5, color:"var(--ink-3)"}}>{t.sub}</div>
</div>
</div>
{steps.map((s,i)=><Step key={i} n={i+1}>{s}</Step>)}
<SuccessStep>
Тумблер зелёный вы подключены. Значок VPN в строке статуса.
</SuccessStep>
</Card>
);
}
function FineNote({ children }){
return (
<div style={{marginTop:10, padding:"10px 14px", borderRadius:12,
background:"var(--bg-tint)", border:"1px solid var(--line-2)",
fontSize:13.5, color:"var(--ink-3)"}}>
{children}
</div>
);
}
// ───────── Region change steps ─────────
function RegionCard({ os }){
const cfg = {
ios:{name:"Смена региона App Store на iPhone", sub:"iOS · Apple ID", color:"linear-gradient(160deg,#ffd166,#ff9500)"},
android:{name:"Смена региона Google Play", sub:"Android · Google аккаунт", color:"linear-gradient(160deg,#7ce0a2,#2cb67d)"},
windows:{name:"Смена региона Microsoft Store", sub:"Windows 10 / 11", color:"linear-gradient(160deg,#7ec2ff,#0a84ff)"},
macos:{name:"Смена региона Mac App Store", sub:"macOS", color:"linear-gradient(160deg,#bdbdc0,#6e6e73)"},
};
const t = cfg[os] || cfg.ios;
let steps;
if (os==="ios" || os==="macos") steps = [
<>Подключите VPN к серверу нужной страны</>,
<>Откройте <strong>«Настройки»</strong> ваше имя <strong>«Медиаматериалы и покупки»</strong></>,
<>Нажмите <strong>«Просмотреть учётную запись»</strong> → подтвердите вход</>,
<>Тапните <strong>«Страна или регион»</strong> <strong>«Изменить страну или регион»</strong></>,
<>Выберите страну, примите условия, заполните адрес и способ оплаты <strong>«Нет»</strong></>,
];
else if (os==="android") steps = [
<>Подключите VPN к серверу нужной страны</>,
<>Очистите кеш и данные <strong>Google Play Store</strong> в настройках устройства</>,
<>Откройте Play Store меню <strong>«Настройки» «Общие» «Настройки аккаунта»</strong></>,
<>Выберите <strong>«Страна и профили»</strong>, подтвердите смену</>,
<>Дождитесь до 24 часов и проверьте регион обновится</>,
];
else steps = [
<>Подключите VPN к серверу нужной страны</>,
<>Откройте <strong>«Параметры» «Время и язык» «Язык и регион»</strong></>,
<>В разделе <strong>«Страна или регион»</strong> выберите нужную страну</>,
<>Перезапустите Microsoft Store</>,
<>Войдите заново под нужным аккаунтом</>,
];
return (
<Card padding={0} style={{overflow:"hidden"}}>
<div style={{display:"flex", alignItems:"center", gap:14, padding:"22px 22px 18px"}}>
<GlyphTile size={48} radius={14} gradient={t.color} glyph="🛍️"/>
<div>
<h2 style={{margin:0, fontSize:20, fontWeight:700, letterSpacing:"-0.015em"}}>{t.name}</h2>
<div style={{marginTop:3, fontSize:13.5, color:"var(--ink-3)"}}>{t.sub}</div>
</div>
</div>
{steps.map((s,i)=><Step key={i} n={i+1}>{s}</Step>)}
<SuccessStep>
Регион изменён теперь доступны приложения этой страны.
</SuccessStep>
</Card>
);
}
// ───────── Verify section ─────────
function VerifySection(){
return (
<section style={{marginTop:48, maxWidth:1020, marginLeft:"auto", marginRight:"auto", padding:"0 28px"}}>
<h3 style={{margin:"0 0 16px", fontSize:20, fontWeight:700, letterSpacing:"-0.015em"}}>
Как проверить, что VPN работает?
</h3>
<div style={{display:"grid", gridTemplateColumns:"repeat(3,1fr)", gap:14}}>
{[
{n:1, text:<>Откройте <code style={cstyle()}>2ip.ru</code> или <code style={cstyle()}>whatismyip.com</code></>},
{n:2, text:<>Страна должна измениться на страну вашего сервера</>},
{n:3, text:<>Попробуйте открыть нужный сайт загрузится</>},
].map(c=>(
<Card key={c.n} padding={18}>
<div style={{display:"flex", alignItems:"flex-start", gap:12}}>
<div style={{
width:30, height:30, borderRadius:999, flexShrink:0,
background:"var(--accent-soft)", color:"var(--accent-2)",
display:"grid", placeItems:"center", fontWeight:700, fontSize:14
}}>{c.n}</div>
<div style={{fontSize:14.5, lineHeight:1.55, color:"var(--ink)"}}>{c.text}</div>
</div>
</Card>
))}
</div>
</section>
);
}
function cstyle(){
return { background:"var(--bg-tint)", border:"1px solid var(--line)",
padding:"2px 8px", borderRadius:8, fontSize:12.5, color:"var(--ink-2)" };
}
// ───────── Troubleshooting ─────────
function Troubleshooting(){
const items = [
{ title:"VPN не подключается", icon:"refresh", color:"linear-gradient(160deg,#ff8a8a,#ff3b30)", points:[
"Перезапустите приложение",
"Выключите/включите Wi-Fi или мобильный интернет",
"Удалите туннель и добавьте конфиг заново",
"Попробуйте другую сеть",
]},
{ title:"VPN включён, нет интернета", icon:"globe", color:"linear-gradient(160deg,#ffd166,#ff9500)", points:[
"Отключитесь и подключитесь снова",
"Убедитесь, что конфиг актуальный",
"Перезагрузите устройство",
"Напишите в поддержку",
]},
{ title:"Сайты не открываются", icon:"search", color:"linear-gradient(160deg,#7ec2ff,#0a84ff)", points:[
"Очистите кеш браузера",
"Смените DNS на 1.1.1.1",
"Откройте в режиме инкогнито",
"Попробуйте другой браузер",
]},
{ title:"Медленная скорость", icon:"gauge", color:"linear-gradient(160deg,#a385ff,#6a44f5)", points:[
"Проверьте скорость без VPN",
"Переподключитесь",
"Закройте лишние приложения",
"Переключитесь Wi-Fi ↔ 4G",
]},
];
return (
<section style={{marginTop:40, maxWidth:1020, marginLeft:"auto", marginRight:"auto", padding:"0 28px"}}>
<h3 style={{margin:"0 0 16px", fontSize:20, fontWeight:700, letterSpacing:"-0.015em"}}>
Что делать, если что-то пошло не так
</h3>
<div style={{display:"grid", gridTemplateColumns:"repeat(4,1fr)", gap:14}}>
{items.map(it=>(
<Card key={it.title} padding={18}>
<div style={{display:"flex", alignItems:"center", gap:10, marginBottom:12}}>
<GlyphTile size={32} radius={10} gradient={it.color} icon={it.icon}/>
<div style={{fontSize:14.5, fontWeight:700, letterSpacing:"-0.005em"}}>{it.title}</div>
</div>
<ul style={{margin:0, padding:0, listStyle:"none", display:"flex",
flexDirection:"column", gap:8}}>
{it.points.map((p,i)=>(
<li key={i} style={{display:"flex", alignItems:"flex-start", gap:8,
fontSize:13.5, color:"var(--ink-2)", lineHeight:1.45}}>
<span style={{width:5, height:5, borderRadius:999, background:"var(--accent)",
marginTop:7, flexShrink:0}}/>
<span>{p}</span>
</li>
))}
</ul>
</Card>
))}
</div>
</section>
);
}
// ───────── Footer ─────────
function Footer(){
return (
<footer style={{maxWidth:760, margin:"56px auto 40px", padding:"0 28px", textAlign:"center"}}>
<p style={{margin:0, fontSize:14, color:"var(--ink-2)"}}>
Возникли проблемы с подключением <a href="https://t.me/PCA_Amnezia_support_bot" target="_blank" rel="noreferrer" style={{color:"var(--accent-2)", fontWeight:600, display:"inline-flex", alignItems:"center", gap:6}}><Icon name="plane" size={14}/> напишите в Telegram-бот @PCA_Amnezia_support_bot</a>.
</p>
<p style={{margin:"10px 0 0", fontSize:12.5, color:"var(--ink-3)"}}>
Страница только для приглашённых пользователей. Не передавайте конфиг третьим лицам.
</p>
</footer>
);
}
// ───────── Main App ─────────
function ConnectApp(){
const initialMode = (typeof URLSearchParams !== "undefined" && new URLSearchParams(location.search).get("mode") === "region")
? "region" : "connect";
const [mode, setMode] = useStateC(initialMode); // connect | region
const [protocol, setProtocol] = useStateC("wg"); // wg | awg
const [os, setOs] = useStateC("ios");
useEffectC(() => {
const params = new URLSearchParams(location.search);
if (params.get("mode") === "region") setMode("region");
}, []);
return (
<div>
<SiteNav brand="Кабинет студента" brandSub="VPN · РЕГИОН · AMNEZIA" />
<Hero/>
<div style={{maxWidth:880, margin:"0 auto", padding:"0 28px"}}>
<ServerCard/>
{/* Mode big segment */}
<div style={{margin:"22px 0 14px"}}>
<BigSegment value={mode} onChange={setMode} options={[
{id:"connect", label:"Подключение VPN"},
{id:"region", label:"Смена региона App Store"},
]}/>
</div>
{/* Protocol (only for connect mode) */}
{mode==="connect" && (
<div style={{marginBottom:14}}>
<BigSegment value={protocol} onChange={setProtocol} options={[
{id:"wg", label:"WireGuard", tag:"стандарт", tagTone:"blue"},
{id:"awg", label:"Amnezia VPN", tag:"обход DPI", tagTone:"purple"},
]}/>
</div>
)}
{/* OS */}
<div style={{marginBottom:18}}>
<OSTabs value={os} onChange={setOs}/>
</div>
{/* Steps */}
{mode==="connect"
? <StepsCard protocol={protocol} os={os}/>
: <RegionCard os={os}/>}
</div>
<VerifySection/>
<Troubleshooting/>
<Footer/>
</div>
);
}
ReactDOM.createRoot(document.getElementById("root")).render(<ConnectApp/>);