Files
Solo_bot/SoloNet Cabinet.html
T

806 lines
46 KiB
HTML
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.
<!doctype html>
<html lang="ru" data-theme="dark">
<head>
<meta charset="utf-8" />
<title>SoloNet — Кабинет</title>
<meta name="viewport" content="width=device-width,initial-scale=1" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
*,*::before,*::after{box-sizing:border-box}
html,body{margin:0;padding:0;height:100%}
:root{
--bg:#0A0A0A; --bg-2:#111; --bg-3:#181818;
--fg:#F2F2F2; --fg-dim:#7A7A7A; --fg-mute:#3A3A3A;
--line:#1F1F1F; --line-2:#2A2A2A;
--red:#FF2D2D; --green:#2DD46F; --yellow:#FFC107;
--mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,monospace;
}
body{background:var(--bg);color:var(--fg);font-family:var(--mono);font-size:13px;line-height:1.5;-webkit-font-smoothing:antialiased}
::selection{background:var(--red);color:#fff}
button{font-family:inherit;cursor:pointer}
.app{display:grid;grid-template-columns:240px 1fr;min-height:100vh}
/* sidebar */
aside{border-right:1px solid var(--line);background:var(--bg);display:flex;flex-direction:column;position:sticky;top:0;height:100vh}
.brand{display:flex;align-items:center;gap:10px;padding:18px 20px;border-bottom:1px solid var(--line);font-weight:700;letter-spacing:.02em}
.brand-mark{width:18px;height:18px;border:1.5px solid var(--fg);position:relative;flex-shrink:0}
.brand-mark::before{content:"";position:absolute;inset:2.5px;background:var(--red)}
.brand-name{font-size:14px;letter-spacing:.04em}
.brand-ver{font-size:9px;color:var(--fg-dim);letter-spacing:.16em;margin-left:auto}
.conn-card{padding:14px 16px;border-bottom:1px solid var(--line);display:flex;flex-direction:column;gap:6px}
.conn-row{display:flex;justify-content:space-between;align-items:center;font-size:10px;color:var(--fg-dim);letter-spacing:.14em;text-transform:uppercase}
.conn-status{display:flex;align-items:center;gap:8px;font-size:13px;color:var(--green);font-weight:600;letter-spacing:.04em}
.conn-dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 0 0 var(--green);animation:pulse 1.6s infinite}
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(45,212,111,.6)}70%{box-shadow:0 0 0 8px transparent}100%{box-shadow:0 0 0 0 transparent}}
.conn-loc{font-size:11px;color:var(--fg-dim);font-variant-numeric:tabular-nums}
nav.tabs{flex:1;padding:14px 8px;display:flex;flex-direction:column;gap:1px;overflow-y:auto}
.tab{display:flex;align-items:center;gap:12px;padding:10px 12px;border:0;background:transparent;color:var(--fg-dim);font:inherit;text-align:left;border-left:2px solid transparent;letter-spacing:.02em;font-size:12px;text-transform:uppercase;letter-spacing:.08em}
.tab .num{font-size:9px;color:var(--fg-mute);width:18px}
.tab .ic{width:14px;height:14px;flex-shrink:0;stroke:currentColor;fill:none;stroke-width:1.4}
.tab .lbl{flex:1}
.tab .badge{font-size:9px;background:var(--red);color:#fff;padding:1px 6px;letter-spacing:.1em}
.tab:hover{color:var(--fg);background:var(--bg-2)}
.tab.active{color:var(--fg);background:var(--bg-2);border-left-color:var(--red)}
.tab.active .num{color:var(--red)}
.side-foot{padding:16px;border-top:1px solid var(--line);font-size:10px;color:var(--fg-dim);letter-spacing:.12em;display:flex;flex-direction:column;gap:8px}
.user{display:flex;align-items:center;gap:10px;text-transform:none;letter-spacing:0;font-size:12px;color:var(--fg)}
.avatar{width:28px;height:28px;border:1px solid var(--line-2);display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:var(--red);flex-shrink:0}
.user-meta{flex:1;min-width:0}
.user-name{font-weight:600;font-size:12px}
.user-plan{font-size:10px;color:var(--fg-dim);letter-spacing:.14em;text-transform:uppercase}
.icon-btn{width:24px;height:24px;border:1px solid var(--line-2);background:transparent;color:var(--fg-dim);display:inline-flex;align-items:center;justify-content:center;font-size:11px}
.icon-btn:hover{color:var(--fg);border-color:var(--fg-dim)}
/* main */
main{display:flex;flex-direction:column;min-width:0}
.topbar{display:flex;align-items:center;justify-content:space-between;padding:14px 28px;border-bottom:1px solid var(--line);position:sticky;top:0;background:color-mix(in oklab, var(--bg) 88%, transparent);backdrop-filter:blur(12px);z-index:10}
.crumbs{display:flex;align-items:center;gap:10px;font-size:11px;color:var(--fg-dim);letter-spacing:.14em;text-transform:uppercase}
.crumbs b{color:var(--fg);font-weight:600}
.crumbs .sep{color:var(--fg-mute)}
.top-actions{display:flex;align-items:center;gap:10px}
.search{display:flex;align-items:center;gap:8px;padding:6px 10px;border:1px solid var(--line-2);background:var(--bg-2);font-size:11px;color:var(--fg-dim);min-width:240px}
.search input{background:transparent;border:0;color:var(--fg);font:inherit;outline:none;flex:1;font-size:12px}
.kbd{font-size:9px;border:1px solid var(--line-2);padding:1px 5px;color:var(--fg-dim);letter-spacing:.06em}
.btn{display:inline-flex;align-items:center;gap:8px;height:32px;padding:0 14px;border:1px solid var(--fg);background:var(--fg);color:var(--bg);font-family:var(--mono);font-weight:600;font-size:11px;letter-spacing:.06em;text-transform:uppercase;border-radius:0;transition:transform .12s, box-shadow .18s}
.btn:hover{transform:translate(-1px,-1px);box-shadow:3px 3px 0 var(--red)}
.btn-ghost{background:transparent;color:var(--fg)}
.btn-ghost:hover{background:var(--fg);color:var(--bg);box-shadow:none;transform:none}
.btn-red{background:var(--red);border-color:var(--red);color:#fff}
.btn-red:hover{box-shadow:3px 3px 0 var(--fg)}
.btn-sm{height:26px;padding:0 10px;font-size:10px}
/* content area */
.content{padding:28px;display:flex;flex-direction:column;gap:24px;min-height:0}
/* page header */
.page-h{display:flex;justify-content:space-between;align-items:flex-end;gap:24px}
.eyebrow{font-size:10px;color:var(--red);letter-spacing:.22em;text-transform:uppercase;display:inline-flex;align-items:center;gap:8px}
.eyebrow::before{content:"";width:18px;height:1px;background:var(--red)}
.page-title{font-size:28px;line-height:1;letter-spacing:-.02em;font-weight:700;margin:8px 0 0;text-transform:uppercase}
.page-meta{font-size:11px;color:var(--fg-dim);letter-spacing:.14em;text-transform:uppercase;text-align:right}
/* grid panels */
.grid{display:grid;gap:16px}
.grid-3{grid-template-columns:1.4fr 1fr 1fr}
.grid-2{grid-template-columns:2fr 1fr}
.grid-4{grid-template-columns:repeat(4,1fr)}
.panel{border:1px solid var(--line);background:var(--bg);display:flex;flex-direction:column;min-height:0}
.panel-h{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;border-bottom:1px solid var(--line);font-size:10px;color:var(--fg-dim);letter-spacing:.16em;text-transform:uppercase}
.panel-h b{color:var(--fg);font-weight:600}
.panel-h .red{color:var(--red)}
.panel-b{padding:18px 16px;flex:1}
/* connect block */
.connect{padding:24px;display:grid;grid-template-columns:auto 1fr;gap:24px;align-items:center}
.conn-btn{width:120px;height:120px;border:2px solid var(--red);background:var(--red);color:#fff;font-family:var(--mono);font-weight:800;font-size:13px;letter-spacing:.12em;text-transform:uppercase;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;border-radius:0;position:relative;transition:all .15s}
.conn-btn::after{content:"";position:absolute;inset:-6px;border:1px solid var(--red);opacity:.4;pointer-events:none}
.conn-btn .ico{font-size:28px;line-height:1}
.conn-btn:hover{box-shadow:6px 6px 0 var(--fg)}
.conn-btn[data-on="0"]{background:transparent;color:var(--fg);border-color:var(--fg-dim)}
.conn-btn[data-on="0"]::after{border-color:var(--fg-dim)}
.conn-info{display:flex;flex-direction:column;gap:8px}
.conn-info .big{font-size:24px;font-weight:700;letter-spacing:-.01em;line-height:1;text-transform:uppercase}
.conn-info .big .accent{color:var(--green)}
.conn-info .row{display:flex;gap:24px;font-size:11px;color:var(--fg-dim);letter-spacing:.12em;text-transform:uppercase;margin-top:4px}
.conn-info .row .v{color:var(--fg);font-weight:600;margin-left:6px;font-variant-numeric:tabular-nums}
.conn-meta-line{font-size:10px;color:var(--fg-mute);letter-spacing:.16em;font-family:var(--mono)}
/* stats row */
.stat{padding:18px 16px}
.stat-k{font-size:10px;color:var(--fg-dim);letter-spacing:.14em;text-transform:uppercase}
.stat-v{font-size:28px;font-weight:700;letter-spacing:-.01em;margin-top:6px;font-variant-numeric:tabular-nums;line-height:1}
.stat-v .u{color:var(--red);font-size:14px;margin-left:4px}
.stat-d{font-size:10px;color:var(--green);margin-top:6px;letter-spacing:.1em}
.stat-d.down{color:var(--red)}
/* charts */
.chart{height:180px;width:100%}
.chart-foot{display:flex;justify-content:space-between;font-size:10px;color:var(--fg-dim);letter-spacing:.12em;padding:10px 16px;border-top:1px solid var(--line)}
/* server list */
.srv-list{display:flex;flex-direction:column}
.srv{display:grid;grid-template-columns:auto 1fr auto auto auto;gap:14px;align-items:center;padding:11px 16px;border-bottom:1px solid var(--line);font-size:12px}
.srv:last-child{border-bottom:0}
.srv:hover{background:var(--bg-2)}
.srv.active{background:var(--bg-2);border-left:2px solid var(--red);padding-left:14px}
.flag{width:22px;height:16px;border:1px solid var(--line-2);font-size:9px;display:inline-flex;align-items:center;justify-content:center;font-weight:700;letter-spacing:.04em;color:var(--fg-dim)}
.srv .name{font-weight:600}
.srv .city{color:var(--fg-dim);font-size:11px;margin-left:8px}
.srv .ping{font-variant-numeric:tabular-nums;color:var(--fg-dim);font-size:11px;letter-spacing:.06em}
.srv .ping b{color:var(--fg)}
.load-bar{width:60px;height:4px;background:var(--line);position:relative}
.load-bar i{position:absolute;left:0;top:0;height:100%;background:var(--green)}
.load-bar i.med{background:var(--yellow)}
.load-bar i.high{background:var(--red)}
.srv .pick{font-size:10px;letter-spacing:.12em;color:var(--fg-dim);text-transform:uppercase}
.srv:hover .pick{color:var(--red)}
.srv.active .pick{color:var(--red)}
/* feed */
.feed{font-size:11px;font-family:var(--mono);padding:8px 16px;max-height:240px;overflow-y:auto}
.feed-line{display:flex;gap:10px;padding:4px 0;color:var(--fg-dim);font-variant-numeric:tabular-nums}
.feed-line .ts{color:var(--fg-mute);flex-shrink:0}
.feed-line .lvl{flex-shrink:0;width:48px;font-weight:700}
.feed-line .lvl.ok{color:var(--green)}
.feed-line .lvl.warn{color:var(--yellow)}
.feed-line .lvl.err{color:var(--red)}
.feed-line .msg{color:var(--fg)}
/* devices grid */
.dev{padding:16px;border:1px solid var(--line);background:var(--bg);display:flex;flex-direction:column;gap:10px}
.dev .top{display:flex;justify-content:space-between;align-items:flex-start}
.dev-name{font-size:13px;font-weight:600}
.dev-id{font-size:10px;color:var(--fg-dim);letter-spacing:.14em}
.dev .body{font-size:11px;color:var(--fg-dim);display:flex;flex-direction:column;gap:4px}
.dev .body b{color:var(--fg);font-weight:500}
.dev .foot{display:flex;justify-content:space-between;align-items:center;border-top:1px dashed var(--line-2);padding-top:10px}
.dev .pill{font-size:9px;letter-spacing:.14em;text-transform:uppercase;padding:3px 6px;border:1px solid var(--green);color:var(--green)}
.dev .pill.off{border-color:var(--fg-mute);color:var(--fg-dim)}
/* invoice list */
.inv{display:grid;grid-template-columns:120px 1fr auto auto auto;gap:16px;padding:11px 16px;border-bottom:1px solid var(--line);font-size:12px;align-items:center}
.inv:last-child{border-bottom:0}
.inv .date{font-variant-numeric:tabular-nums;color:var(--fg-dim);letter-spacing:.06em}
.inv .label{font-weight:500}
.inv .amount{font-variant-numeric:tabular-nums;font-weight:700}
.inv .pill{font-size:9px;letter-spacing:.14em;text-transform:uppercase;padding:3px 6px;border:1px solid var(--green);color:var(--green)}
.inv .pill.fail{border-color:var(--red);color:var(--red)}
.inv .dl{font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--fg-dim)}
.inv .dl:hover{color:var(--red)}
/* settings rows */
.opt{display:flex;justify-content:space-between;align-items:center;padding:14px 16px;border-bottom:1px solid var(--line);gap:16px}
.opt:last-child{border-bottom:0}
.opt .opt-l{display:flex;flex-direction:column;gap:2px}
.opt .opt-name{font-size:13px;font-weight:500}
.opt .opt-d{font-size:11px;color:var(--fg-dim);max-width:60ch}
.toggle{position:relative;width:36px;height:20px;border:0;border-radius:0;background:var(--line);transition:background .15s;padding:0;border:1px solid var(--line-2)}
.toggle[data-on="1"]{background:var(--red);border-color:var(--red)}
.toggle i{position:absolute;top:1px;left:1px;width:16px;height:16px;background:#fff;transition:transform .15s}
.toggle[data-on="1"] i{transform:translateX(16px)}
.seg{display:inline-flex;border:1px solid var(--line-2)}
.seg button{appearance:none;background:transparent;border:0;color:var(--fg-dim);padding:6px 10px;font:inherit;font-size:10px;letter-spacing:.14em;text-transform:uppercase;border-right:1px solid var(--line-2)}
.seg button:last-child{border-right:0}
.seg button.on{background:var(--fg);color:var(--bg)}
/* support */
.ticket{padding:14px 16px;border-bottom:1px solid var(--line);display:flex;justify-content:space-between;gap:16px;align-items:center}
.ticket:last-child{border-bottom:0}
.ticket .meta{font-size:10px;color:var(--fg-dim);letter-spacing:.14em;text-transform:uppercase}
.ticket .subj{font-size:13px;margin-top:4px}
.ticket .right{text-align:right;font-size:11px;color:var(--fg-dim)}
/* security */
.sec-row{display:flex;align-items:center;gap:14px;padding:14px 16px;border-bottom:1px solid var(--line)}
.sec-row:last-child{border-bottom:0}
.sec-ico{width:32px;height:32px;border:1px solid var(--line-2);display:flex;align-items:center;justify-content:center;color:var(--green);flex-shrink:0}
.sec-ico.warn{color:var(--yellow);border-color:var(--yellow)}
.sec-ico svg{width:16px;height:16px;stroke:currentColor;fill:none;stroke-width:1.5}
.sec-row .body{flex:1}
.sec-row .body b{display:block;font-size:13px;margin-bottom:2px}
.sec-row .body span{font-size:11px;color:var(--fg-dim)}
/* referral */
.ref-card{padding:24px 20px;border:1px solid var(--red);background:linear-gradient(180deg, color-mix(in oklab, var(--red) 12%, transparent) 0%, transparent 100%)}
.ref-num{font-size:42px;font-weight:800;letter-spacing:-.02em;line-height:1}
.ref-num .u{color:var(--red);font-size:18px;margin-left:4px}
.ref-link{display:flex;align-items:center;border:1px solid var(--line-2);background:var(--bg-2);padding:10px 14px;margin-top:18px;font-size:12px;justify-content:space-between;gap:12px}
.ref-link code{color:var(--fg);font-family:var(--mono);font-size:12px;letter-spacing:0}
.empty{padding:40px 16px;text-align:center;color:var(--fg-dim);font-size:12px}
.empty-big{font-size:18px;color:var(--fg);margin-bottom:8px;letter-spacing:.04em;text-transform:uppercase}
@media (max-width: 1100px){
.grid-3{grid-template-columns:1fr 1fr}
.grid-4{grid-template-columns:repeat(2,1fr)}
}
@media (max-width: 760px){
.app{grid-template-columns:1fr}
aside{position:relative;height:auto}
.grid-3,.grid-4,.grid-2{grid-template-columns:1fr}
}
</style>
</head>
<body>
<div id="root"></div>
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<script type="text/babel" data-presets="env,react">
const { useState, useEffect, useRef, useMemo } = React;
// ───────────────────── Icons ─────────────────────
const Ico = {
prof: <svg className="ic" viewBox="0 0 16 16"><circle cx="8" cy="5.5" r="2.6"/><path d="M2.5 14c.6-2.6 2.9-4.2 5.5-4.2S13 11.4 13.5 14"/></svg>,
bill: <svg className="ic" viewBox="0 0 16 16"><rect x="2" y="3" width="12" height="10"/><path d="M2 6h12"/><path d="M5 10h2"/></svg>,
ref: <svg className="ic" viewBox="0 0 16 16"><circle cx="5" cy="5" r="2"/><circle cx="11" cy="11" r="2"/><path d="M5 7v2a2 2 0 0 0 2 2h2"/></svg>,
gift: <svg className="ic" viewBox="0 0 16 16"><rect x="2" y="6" width="12" height="8"/><path d="M2 9h12M8 6v8"/><path d="M5 6c0-1.5 1-2.5 2-2.5S8 4 8 6c0-2 0-2.5 1-2.5S11 4.5 11 6"/></svg>,
bell: <svg className="ic" viewBox="0 0 16 16"><path d="M4 11V7a4 4 0 0 1 8 0v4l1 1.5H3z"/><path d="M6.5 13.5a1.5 1.5 0 0 0 3 0"/></svg>,
};
// ───────────────────── Tabs definition ─────────────────────
const TABS = [
{ id: 'prof', num: '01', label: 'Профиль', icon: Ico.prof },
{ id: 'bill', num: '02', label: 'Подписки', icon: Ico.bill },
{ id: 'ref', num: '03', label: 'Партнёрка', icon: Ico.ref },
{ id: 'gift', num: '04', label: 'Подарки', icon: Ico.gift, badge: '2' },
{ id: 'note', num: '05', label: 'Уведомления', icon: Ico.bell, badge: '5' },
];
// ───────────────────── Server data ─────────────────────
const SERVERS = [
{ code:'NL', city:'Amsterdam', name:'AMS-03', ping:14, load:'low', loadV:22, hops:1 },
{ code:'DE', city:'Frankfurt', name:'FRA-07', ping:18, load:'low', loadV:31 },
{ code:'CH', city:'Zürich', name:'ZRH-02', ping:24, load:'low', loadV:18 },
{ code:'SE', city:'Stockholm', name:'STO-01', ping:32, load:'med', loadV:54 },
{ code:'IS', city:'Reykjavík', name:'KEF-01', ping:48, load:'med', loadV:62 },
{ code:'US', city:'New York', name:'NYC-09', ping:74, load:'low', loadV:38 },
{ code:'US', city:'Los Angeles', name:'LAX-04', ping:128, load:'high', loadV:84 },
{ code:'JP', city:'Tokyo', name:'TYO-02', ping:172, load:'med', loadV:48 },
{ code:'SG', city:'Singapore', name:'SIN-03', ping:188, load:'low', loadV:24 },
];
// ───────────────────── Sparkline ─────────────────────
function Sparkline({ data, color = 'var(--red)', fill = true, height = 180 }){
const ref = useRef(null);
useEffect(() => {
const c = ref.current; if (!c) return;
const ctx = c.getContext('2d');
const dpr = window.devicePixelRatio || 1;
const w = c.clientWidth, h = c.clientHeight;
c.width = w*dpr; c.height = h*dpr; ctx.setTransform(dpr,0,0,dpr,0,0);
ctx.clearRect(0,0,w,h);
// grid
ctx.strokeStyle = 'rgba(245,245,245,0.05)'; ctx.lineWidth = 1; ctx.beginPath();
for (let i=1;i<5;i++){ const y = (h/5)*i; ctx.moveTo(0,y); ctx.lineTo(w,y); }
ctx.stroke();
// data
const max = Math.max(...data) || 1;
const min = Math.min(...data);
const norm = data.map(v => (v - min) / (max - min || 1));
ctx.lineWidth = 1.5;
ctx.strokeStyle = color;
if (fill){
const grad = ctx.createLinearGradient(0,0,0,h);
grad.addColorStop(0, color === 'var(--red)' ? 'rgba(255,45,45,0.35)' : 'rgba(45,212,111,0.35)');
grad.addColorStop(1, 'rgba(0,0,0,0)');
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(0, h - norm[0]*(h-20) - 8);
for (let i=1;i<norm.length;i++){
const x = (i/(norm.length-1))*w;
const y = h - norm[i]*(h-20) - 8;
ctx.lineTo(x,y);
}
ctx.lineTo(w,h); ctx.lineTo(0,h); ctx.closePath(); ctx.fill();
}
ctx.beginPath();
for (let i=0;i<norm.length;i++){
const x = (i/(norm.length-1))*w;
const y = h - norm[i]*(h-20) - 8;
i===0 ? ctx.moveTo(x,y) : ctx.lineTo(x,y);
}
ctx.stroke();
}, [data, color, fill]);
return <canvas ref={ref} className="chart" style={{height}}/>;
}
// ───────────────────── Sidebar ─────────────────────
function Sidebar({ active, setActive }){
return (
<aside>
<div className="brand">
<span className="brand-mark"></span>
<span className="brand-name">SOLONET</span>
<span className="brand-ver">v 4.2.0</span>
</div>
<div className="conn-card">
<div className="conn-row"><span>// TUNNEL</span><span>WG-01</span></div>
<div className="conn-status"><span className="conn-dot"></span> CONNECTED</div>
<div className="conn-loc">NL · AMS-03 · 14 ms</div>
</div>
<nav className="tabs">
{TABS.map(t => (
<button key={t.id} className={`tab ${active===t.id?'active':''}`} onClick={() => setActive(t.id)}>
<span className="num">{t.num}</span>
{t.icon}
<span className="lbl">{t.label}</span>
{t.badge && <span className="badge">{t.badge}</span>}
</button>
))}
</nav>
<div className="side-foot">
<div className="user">
<div className="avatar">SK</div>
<div className="user-meta">
<div className="user-name">solo_kirill</div>
<div className="user-plan">PRIME · 247 дн</div>
</div>
<button className="icon-btn" title="Выйти"></button>
</div>
</div>
</aside>
);
}
// ───────────────────── Topbar ─────────────────────
function Topbar({ active }){
const tab = TABS.find(t => t.id === active);
return (
<div className="topbar">
<div className="crumbs">
<span>Кабинет</span><span className="sep">/</span>
<b>{tab.label}</b>
<span className="sep">·</span>
<span>NODE-AMS-03</span>
</div>
<div className="top-actions">
<div className="search">
<span></span>
<input placeholder="Поиск серверов, устройств…" />
<span className="kbd"> K</span>
</div>
<button className="btn btn-sm btn-ghost">Помощь</button>
<button className="btn btn-sm btn-red">Купить трафик</button>
</div>
</div>
);
}
// ───────────────────── Page: Profile ─────────────────────
function ProfilePage(){
const [twofa, setTwofa] = useState(false);
const [news, setNews] = useState(true);
return (
<>
<div className="page-h">
<div>
<span className="eyebrow">/// Личные данные</span>
<h1 className="page-title">Профиль</h1>
</div>
<div className="page-meta">ID · SK-9F3A2C</div>
</div>
<div className="grid grid-2">
<div className="panel">
<div className="panel-h"><b>// Учётная запись</b><span>аккаунт</span></div>
<div className="panel-b" style={{display:'flex',flexDirection:'column',gap:14}}>
<div style={{display:'flex',gap:14,alignItems:'center'}}>
<div className="avatar" style={{width:56,height:56,fontSize:18}}>SK</div>
<div>
<div style={{fontSize:16,fontWeight:600}}>solo_kirill</div>
<div style={{fontSize:11,color:'var(--fg-dim)',letterSpacing:'.12em',textTransform:'uppercase',marginTop:4}}>с 04 апр 2024 · 763 дня</div>
</div>
<button className="btn btn-sm btn-ghost" style={{marginLeft:'auto'}}>Сменить</button>
</div>
<div style={{height:1,background:'var(--line)'}}></div>
<Field k="E-mail" v="kirill@solonet.io" verified />
<Field k="Телефон" v="+7 ··· ··· 47-12" />
<Field k="Telegram" v="@solo_kirill" verified />
<Field k="Язык" v="Русский" />
</div>
</div>
<div className="panel">
<div className="panel-h"><b>// Безопасность</b><span>защита</span></div>
<div className="opt">
<div className="opt-l"><div className="opt-name">Двухфакторная (2FA)</div><div className="opt-d">Подтверждение по TOTP-коду из приложения. Рекомендуется.</div></div>
<button className="toggle" data-on={twofa?'1':'0'} onClick={()=>setTwofa(!twofa)}><i/></button>
</div>
<div className="opt">
<div className="opt-l"><div className="opt-name">Письма с новостями</div><div className="opt-d">Раз в месяц. Никакого спама только важные обновления.</div></div>
<button className="toggle" data-on={news?'1':'0'} onClick={()=>setNews(!news)}><i/></button>
</div>
<div className="opt">
<div className="opt-l"><div className="opt-name">Сменить пароль</div><div className="opt-d">Последнее изменение 47 дней назад.</div></div>
<button className="btn btn-sm btn-ghost">Изменить</button>
</div>
<div className="opt">
<div className="opt-l"><div className="opt-name" style={{color:'var(--red)'}}>Удалить аккаунт</div><div className="opt-d">Все данные стираются с RAM-серверов в течение 24 часов.</div></div>
<button className="btn btn-sm btn-ghost" style={{borderColor:'var(--red)',color:'var(--red)'}}>Удалить</button>
</div>
</div>
</div>
<div className="panel">
<div className="panel-h"><b>// История входов</b><span>последние 5</span></div>
<div className="srv-list">
{[
['MBP-14 · macOS','198.51.100.91 · NL','сейчас'],
['iPhone 15 Pro','198.51.100.42 · NL','12 мин назад'],
['Web · Firefox','83.220.171.4 · RU','3 ч назад'],
['Router OpenWrt','198.51.100.12 · DE','вчера'],
['ThinkPad X1','—','5 дней назад'],
].map(([n,ip,t],i)=>(
<div className="srv" key={i}>
<span className="flag"></span>
<span><span className="name">{n}</span></span>
<span className="ping">{ip}</span>
<span></span>
<span className="pick">{t}</span>
</div>
))}
</div>
</div>
</>
);
}
function Field({k,v,verified}){
return (
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',gap:12,padding:'8px 0',borderBottom:'1px dashed var(--line)'}}>
<span style={{fontSize:11,color:'var(--fg-dim)',letterSpacing:'.12em',textTransform:'uppercase'}}>{k}</span>
<span style={{display:'flex',alignItems:'center',gap:8}}>
<b style={{fontSize:13,fontWeight:600}}>{v}</b>
{verified && <span style={{fontSize:9,letterSpacing:'.14em',color:'var(--green)',border:'1px solid var(--green)',padding:'2px 5px'}}>VERIFIED</span>}
</span>
</div>
);
}
// ───────────────────── Page: Subscriptions ─────────────────────
function SubsPage(){
const invoices = [
{ date:'2026-04-12', label:'PRIME · 12 мес', amount:'4 788 ₽', status:'PAID' },
{ date:'2025-04-12', label:'PRIME · 12 мес', amount:'4 788 ₽', status:'PAID' },
{ date:'2024-10-04', label:'CORE · 6 мес', amount:'1 494 ₽', status:'PAID' },
{ date:'2024-04-04', label:'PROBE · 1 мес', amount:'99 ₽', status:'FAIL' },
];
return (
<>
<div className="page-h">
<div>
<span className="eyebrow">/// Тарифы и платежи</span>
<h1 className="page-title">Подписки</h1>
</div>
<div className="page-meta">PRIME · ДО 12 АПР 2027</div>
</div>
<div className="grid grid-4">
<Stat k="До продления" v="347" u="дн" d="автопродление активно" />
<Stat k="Списание" v="4 788" u="₽" d="12 апр 2027" />
<Stat k="Сэкономлено" v="3 624" u="₽" d="скидка за год · 39%" />
<Stat k="Бонусных дней" v="+210" d="7 рефералов" />
</div>
<div className="grid grid-2">
<div className="panel">
<div className="panel-h"><b>// Текущий тариф</b><span className="red">PRIME</span></div>
<div className="panel-b" style={{display:'flex',flexDirection:'column',gap:18}}>
<div style={{fontSize:48,fontWeight:800,letterSpacing:'-.02em',lineHeight:1}}>399 <span style={{fontSize:18,color:'var(--fg-dim)'}}> / мес</span></div>
<div style={{fontSize:11,color:'var(--fg-dim)',letterSpacing:'.14em',textTransform:'uppercase'}}>Списание 4 788 12 АПР 2027</div>
<div style={{height:1,background:'var(--line)'}}></div>
<ul style={{listStyle:'none',padding:0,margin:0,fontSize:12,display:'flex',flexDirection:'column',gap:8,color:'var(--fg-dim)'}}>
<li style={{display:'flex',justifyContent:'space-between'}}><span>Серверы</span><b style={{color:'var(--fg)'}}>412</b></li>
<li style={{display:'flex',justifyContent:'space-between'}}><span>Устройств</span><b style={{color:'var(--fg)'}}>10</b></li>
<li style={{display:'flex',justifyContent:'space-between'}}><span>Multi-hop</span><b style={{color:'var(--fg)'}}>2 узла</b></li>
<li style={{display:'flex',justifyContent:'space-between'}}><span>Поддержка</span><b style={{color:'var(--fg)'}}>Live · 24/7</b></li>
</ul>
<div style={{display:'flex',gap:10,marginTop:'auto',paddingTop:8}}>
<button className="btn btn-red">Перейти на SHADOW </button>
<button className="btn btn-sm btn-ghost">Отменить</button>
</div>
</div>
</div>
<div className="panel">
<div className="panel-h"><b>// Метод оплаты</b><span>основной</span></div>
<div className="panel-b" style={{display:'flex',flexDirection:'column',gap:14}}>
<div style={{border:'1px solid var(--line-2)',padding:16,display:'flex',justifyContent:'space-between',alignItems:'center'}}>
<div>
<div style={{fontSize:13,fontWeight:600}}> 4 829</div>
<div style={{fontSize:11,color:'var(--fg-dim)',marginTop:4,letterSpacing:'.1em'}}>VISA · ИСТЕКАЕТ 11/28</div>
</div>
<button className="btn btn-sm btn-ghost">Сменить</button>
</div>
<div style={{fontSize:11,color:'var(--fg-dim)',letterSpacing:'.12em',textTransform:'uppercase'}}>// Альтернативные методы</div>
<div style={{display:'flex',gap:8,flexWrap:'wrap'}}>
<span style={{padding:'6px 10px',border:'1px solid var(--line-2)',fontSize:11,letterSpacing:'.08em'}}>SBP</span>
<span style={{padding:'6px 10px',border:'1px solid var(--line-2)',fontSize:11,letterSpacing:'.08em'}}>BTC</span>
<span style={{padding:'6px 10px',border:'1px solid var(--line-2)',fontSize:11,letterSpacing:'.08em'}}>USDT</span>
<span style={{padding:'6px 10px',border:'1px solid var(--line-2)',fontSize:11,letterSpacing:'.08em'}}>MONERO</span>
</div>
</div>
</div>
</div>
<div className="panel">
<div className="panel-h"><b>// История платежей</b><span>{invoices.length} записей</span></div>
<div>
{invoices.map((inv,i) => (
<div className="inv" key={i}>
<span className="date">{inv.date}</span>
<span className="label">{inv.label}</span>
<span className="amount">{inv.amount}</span>
<span className={`pill ${inv.status==='FAIL'?'fail':''}`}>{inv.status}</span>
<a className="dl" href="#"> PDF</a>
</div>
))}
</div>
</div>
</>
);
}
// ───────────────────── Page: Partner ─────────────────────
function PartnerPage(){
return (
<>
<div className="page-h">
<div>
<span className="eyebrow">/// Приведи одного</span>
<h1 className="page-title">Партнёрка</h1>
</div>
<div className="page-meta">+30 ДНЕЙ + 30% за каждого</div>
</div>
<div className="grid grid-4">
<Stat k="Приведено" v="7" d="+2 за месяц" />
<Stat k="Бонус-дней" v="+210" d="к подписке" />
<Stat k="Кэшбек" v="1 050" u="₽" d="к выводу" />
<Stat k="Конверсия" v="58" u="%" d="переход → оплата" />
</div>
<div className="grid grid-2">
<div className="ref-card">
<div style={{fontSize:11,color:'var(--fg-dim)',letterSpacing:'.16em',textTransform:'uppercase',marginBottom:12}}>// твоя реферальная ссылка</div>
<div className="ref-num">SK<span className="u">-9F3A</span></div>
<div className="ref-link">
<code>solonet.io/r/SK-9F3A</code>
<button className="btn btn-sm">Копировать</button>
</div>
<div style={{display:'flex',gap:10,marginTop:18}}>
<button className="btn btn-sm btn-ghost">Поделиться</button>
<button className="btn btn-sm btn-ghost">QR-код</button>
<button className="btn btn-sm btn-ghost">UTM</button>
</div>
</div>
<div className="panel">
<div className="panel-h"><b>// Условия программы</b><span>tier · BRONZE</span></div>
<div className="panel-b" style={{display:'flex',flexDirection:'column',gap:12,fontSize:12,color:'var(--fg-dim)'}}>
<div style={{display:'flex',justifyContent:'space-between'}}><span>Друг получает</span><b style={{color:'var(--fg)'}}>30% на 1-й платёж</b></div>
<div style={{display:'flex',justifyContent:'space-between'}}><span>Ты получаешь</span><b style={{color:'var(--fg)'}}>+30 дней + 30%</b></div>
<div style={{display:'flex',justifyContent:'space-between'}}><span>Активация</span><b style={{color:'var(--fg)'}}>после оплаты</b></div>
<div style={{display:'flex',justifyContent:'space-between'}}><span>Срок жизни ссылки</span><b style={{color:'var(--fg)'}}>30 дней</b></div>
<div style={{height:1,background:'var(--line)',margin:'4px 0'}}></div>
<div style={{fontSize:10,letterSpacing:'.16em',textTransform:'uppercase',color:'var(--fg-dim)'}}>// до SILVER · 13 рефералов</div>
<div style={{height:6,background:'var(--line)',position:'relative'}}><div style={{position:'absolute',left:0,top:0,bottom:0,width:'35%',background:'var(--red)'}}/></div>
</div>
</div>
</div>
<div className="panel">
<div className="panel-h"><b>// Приведённые друзья</b><span>7 человек</span></div>
<div className="srv-list">
{[
['solo_anya','PRIME · 12 мес','+30 дн','2 дня назад'],
['mr_quiet','CORE · 6 мес','+30 дн','1 нед назад'],
['root@ghost','SHADOW · 12 мес','+30 дн','2 нед назад'],
['ave','PROBE','—','1 мес назад'],
['nox_4','CORE · 6 мес','+30 дн','1 мес назад'],
['v.lonely','PRIME · 12 мес','+30 дн','2 мес назад'],
['static_id','PRIME · 12 мес','+30 дн','3 мес назад'],
].map(([n,p,b,t],i)=>(
<div className="srv" key={i}>
<span className="flag">@</span>
<span><span className="name">{n}</span><span className="city">· {p}</span></span>
<span className="ping" style={{color:'var(--green)'}}>{b}</span>
<span></span>
<span className="pick">{t}</span>
</div>
))}
</div>
</div>
</>
);
}
// ───────────────────── Page: Gifts ─────────────────────
function GiftsPage(){
return (
<>
<div className="page-h">
<div>
<span className="eyebrow">/// Промо и бонусы</span>
<h1 className="page-title">Подарки</h1>
</div>
<div className="page-meta">2 АКТИВНЫХ · 1 НОВЫЙ</div>
</div>
<div className="grid grid-3">
{[
{ tag:'NEW', t:'+30 дней SHADOW', d:'Активируй апгрейд за 7 дней — получи +30 дней триала премиум-уровня.', exp:'до 06 МАЯ', cta:'Активировать', red:true },
{ tag:'ACTIVE', t:'50% на год CORE', d:'Промо для друга. Один раз на новый аккаунт.', exp:'до 31 МАЯ', cta:'Скопировать код', code:'SOLO50-FRIEND' },
{ tag:'ACTIVE', t:'+15 дней за 2FA', d:'Включи двухфакторку — получи дополнительные 15 дней к подписке.', exp:'бессрочно', cta:'Включить 2FA' },
].map((g,i)=>(
<div className="panel" key={i} style={g.red?{borderColor:'var(--red)'}:{}}>
<div className="panel-h"><b className={g.red?'red':''}>// {g.tag}</b><span>{g.exp}</span></div>
<div className="panel-b" style={{display:'flex',flexDirection:'column',gap:12,minHeight:160}}>
<div style={{fontSize:22,fontWeight:700,letterSpacing:'-.01em',lineHeight:1.05,textTransform:'uppercase'}}>{g.t}</div>
<div style={{fontSize:12,color:'var(--fg-dim)'}}>{g.d}</div>
{g.code && <div style={{padding:'8px 10px',border:'1px dashed var(--line-2)',fontSize:13,fontWeight:600,letterSpacing:'.08em',color:'var(--red)'}}>{g.code}</div>}
<button className={`btn ${g.red?'btn-red':'btn-ghost'}`} style={{marginTop:'auto'}}>{g.cta} </button>
</div>
</div>
))}
</div>
<div className="grid grid-2">
<div className="panel">
<div className="panel-h"><b>// Промо-код</b><span>введи и активируй</span></div>
<div className="panel-b" style={{display:'flex',gap:10,alignItems:'stretch'}}>
<input placeholder="PROMO-XXXX-XXXX" style={{flex:1,background:'var(--bg-2)',border:'1px solid var(--line-2)',color:'var(--fg)',font:'inherit',padding:'10px 14px',outline:'none',letterSpacing:'.08em'}}/>
<button className="btn btn-red">Активировать</button>
</div>
</div>
<div className="panel">
<div className="panel-h"><b>// История подарков</b><span>5 активаций</span></div>
<div className="srv-list">
{[
['+30 дней триала PRIME','SOLO-FRIEND-9F3A','12 апр 2026'],
['Кэшбек 1 050 ₽','REF-PROGRAM','01 апр 2026'],
['+15 дней за 2FA','—','—'],
['50% на CORE','BLACKFRI-2025','24 ноя 2025'],
].map(([t,c,d],i)=>(
<div className="srv" key={i}>
<span className="flag" style={{color:'var(--red)'}}></span>
<span><span className="name">{t}</span></span>
<span className="ping">{c}</span>
<span></span>
<span className="pick" style={{color:'var(--fg-dim)'}}>{d}</span>
</div>
))}
</div>
</div>
</div>
</>
);
}
// ───────────────────── Page: Notifications ─────────────────────
function NotificationsPage(){
const [filter, setFilter] = useState('all');
const items = [
{ lvl:'WARN', cls:'warn', t:'Замечена попытка входа из другой страны', d:'IP 213.180.42.8 · Германия · браузер Chrome. Если это не вы — смените пароль.', ts:'14:08 · сегодня', unread:true },
{ lvl:'INFO', cls:'', t:'Новый сервер в сети — Лиссабон (LIS-01)', d:'Доступен в тарифах PRIME и SHADOW. Латентность из Москвы — 64 мс.', ts:'10 ч назад', unread:true },
{ lvl:'OK', cls:'ok', t:'Платёж 4 788 ₽ прошёл успешно', d:'Подписка PRIME продлена до 12 апреля 2027.', ts:'12 апр', unread:true },
{ lvl:'INFO', cls:'', t:'Друг solo_anya активировал PRIME · 12 мес', d:'+30 бонусных дней начислено к твоей подписке.', ts:'10 апр', unread:true },
{ lvl:'WARN', cls:'warn', t:'2FA не включён', d:'Рекомендуем настроить — это даст +15 дней подписки в подарок.', ts:'05 апр', unread:true },
{ lvl:'OK', cls:'ok', t:'Утечка данных не обнаружена', d:'Авто-аудит DNS · IPv6 · WebRTC прошёл успешно.', ts:'02 апр' },
{ lvl:'INFO', cls:'', t:'Обновление клиента 4.2.0', d:'Поддержка obfuscated протокола, починка автостарта на macOS Sonoma.', ts:'28 мар' },
{ lvl:'INFO', cls:'', t:'Сменили дизайн кабинета', d:'Добавили вкладки Подарки и Партнёрку — загляни.', ts:'15 мар' },
];
const filtered = filter==='unread' ? items.filter(i=>i.unread) : filter==='warn' ? items.filter(i=>i.lvl==='WARN') : items;
return (
<>
<div className="page-h">
<div>
<span className="eyebrow">/// Системные события</span>
<h1 className="page-title">Уведомления</h1>
</div>
<div className="page-meta">5 НЕПРОЧИТАННЫХ</div>
</div>
<div className="panel">
<div className="panel-h" style={{justifyContent:'space-between'}}>
<b>// Лента событий</b>
<div style={{display:'flex',gap:10,alignItems:'center'}}>
<div className="seg">
{[['all','Все · 8'],['unread','Новые · 5'],['warn','Важные · 2']].map(([k,l]) => (
<button key={k} className={filter===k?'on':''} onClick={()=>setFilter(k)}>{l}</button>
))}
</div>
<button className="btn btn-sm btn-ghost">Пометить как прочитанные</button>
</div>
</div>
<div>
{filtered.map((n,i)=>(
<div key={i} style={{display:'grid',gridTemplateColumns:'auto 60px 1fr auto',gap:14,alignItems:'flex-start',padding:'14px 16px',borderBottom:'1px solid var(--line)',background:n.unread?'var(--bg-2)':'transparent'}}>
<span style={{width:8,height:8,marginTop:6,background:n.unread?'var(--red)':'var(--fg-mute)',display:'inline-block',borderRadius:'50%'}}></span>
<span className={`lvl ${n.cls}`} style={{fontSize:10,fontWeight:700,letterSpacing:'.14em',marginTop:2,color:n.cls==='ok'?'var(--green)':n.cls==='warn'?'var(--yellow)':'var(--fg-dim)'}}>{n.lvl}</span>
<div>
<div style={{fontSize:13,fontWeight:600,marginBottom:3}}>{n.t}</div>
<div style={{fontSize:11,color:'var(--fg-dim)',lineHeight:1.5,maxWidth:'70ch'}}>{n.d}</div>
</div>
<span style={{fontSize:10,color:'var(--fg-dim)',letterSpacing:'.12em',textTransform:'uppercase',whiteSpace:'nowrap',marginTop:2}}>{n.ts}</span>
</div>
))}
</div>
</div>
<div className="panel">
<div className="panel-h"><b>// Каналы доставки</b><span>куда присылать</span></div>
<NotifChannel name="E-mail" v="kirill@solonet.io" on={true}/>
<NotifChannel name="Telegram" v="@solo_kirill" on={true}/>
<NotifChannel name="Push в приложении" v="3 устройства" on={true}/>
<NotifChannel name="SMS" v="+7 ··· ··· 47-12" on={false}/>
</div>
</>
);
}
function NotifChannel({name,v,on:onInit}){
const [on,setOn] = useState(onInit);
return (
<div className="opt">
<div className="opt-l"><div className="opt-name">{name}</div><div className="opt-d">{v}</div></div>
<button className="toggle" data-on={on?'1':'0'} onClick={()=>setOn(!on)}><i/></button>
</div>
);
}
function Stat({ k, v, u, d, down }){
return (
<div className="panel stat">
<div className="stat-k">{k}</div>
<div className="stat-v">{v}{u && <span className="u">{u}</span>}</div>
{d && <div className={`stat-d ${down?'down':''}`}>{down?'↓':'↑'} {d}</div>}
</div>
);
}
// ───────────────────── App ─────────────────────
const PAGES = {
prof: ProfilePage,
bill: SubsPage,
ref: PartnerPage,
gift: GiftsPage,
note: NotificationsPage,
};
function App(){
const [active, setActive] = useState('prof');
const Page = PAGES[active];
return (
<div className="app" data-screen-label={`Cabinet · ${active}`}>
<Sidebar active={active} setActive={setActive}/>
<main>
<Topbar active={active}/>
<div className="content">
<Page />
</div>
</main>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
</script>
</body>
</html>