pipeline and codebase updates
This commit is contained in:
+295
-62
@@ -1,117 +1,350 @@
|
||||
// frontend/src/Profile.tsx
|
||||
import { useEffect, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Shield, Zap, Key, Copy, CreditCard, Loader2 } from "lucide-react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import {
|
||||
Shield,
|
||||
Zap,
|
||||
Copy,
|
||||
Loader2,
|
||||
ArrowUpCircle,
|
||||
Users,
|
||||
Activity,
|
||||
CheckCircle2,
|
||||
Globe,
|
||||
} from "lucide-react";
|
||||
import { fetchProfile } from "./api";
|
||||
|
||||
export default function Profile() {
|
||||
const [user, setUser] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
// --- ЛОКАЛИЗАЦИЯ И СТАТИКА ---
|
||||
const DICT = {
|
||||
en: {
|
||||
auth_ready: "App Authorization Ready",
|
||||
operative: "Operative",
|
||||
balance: "Balance",
|
||||
uplink_status: "Uplink Status",
|
||||
expires: "Expires",
|
||||
no_limit: "Bandwidth limited. Upgrade for max routing speed.",
|
||||
system_override: "System Override",
|
||||
manage_plan: "Manage Plan",
|
||||
acquire: "Acquire Tier",
|
||||
syndicate: "Syndicate Network",
|
||||
recruits: "Recruits",
|
||||
earned: "Eddies Earned",
|
||||
ref_desc: "Expand the grid. Earn 100 E$ per initialized uplink.",
|
||||
copied: "Copied!",
|
||||
plans: {
|
||||
GHOST: {
|
||||
tag: "Personal Uplink",
|
||||
desc: "The ideal choice for personal use. Minimal digital footprint.",
|
||||
features: ["1 device", "ChaCha20-Poly1305", "DPI-Analysis Protection"],
|
||||
},
|
||||
NETRUNNER: {
|
||||
tag: "Professional Interface",
|
||||
desc: "For professionals working with multiple devices simultaneously.",
|
||||
features: ["5 devices", "Priority Support", "Built-in FakeDNS"],
|
||||
},
|
||||
MAINFRAME: {
|
||||
tag: "Command Center",
|
||||
desc: "The ultimate solution for teams and maximum network independence.",
|
||||
features: [
|
||||
"Unlimited devices",
|
||||
"High-Load Ready",
|
||||
"24/7 Priority Support",
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
ru: {
|
||||
auth_ready: "Авторизация Успешна",
|
||||
operative: "Оперативник",
|
||||
balance: "Баланс",
|
||||
uplink_status: "Статус Узла",
|
||||
expires: "Истекает",
|
||||
no_limit: "Скорость ограничена. Повысьте уровень доступа.",
|
||||
system_override: "Терминал Доступа",
|
||||
manage_plan: "Управление подпиской",
|
||||
acquire: "Арендовать",
|
||||
syndicate: "Синдикат",
|
||||
recruits: "Рекруты",
|
||||
earned: "Заработано (E$)",
|
||||
ref_desc: "Расширяй сеть. Получай 100 E$ за каждый новый узел.",
|
||||
copied: "Скопировано!",
|
||||
plans: {
|
||||
GHOST: {
|
||||
tag: "Личный Узел",
|
||||
desc: "Идеально для себя. Минимальный цифровой след.",
|
||||
features: ["1 устройство", "ChaCha20-Poly1305", "Защита от DPI"],
|
||||
},
|
||||
NETRUNNER: {
|
||||
tag: "Про-Интерфейс",
|
||||
desc: "Для работы со смартфоном, ноутом и планшетом одновременно.",
|
||||
features: ["5 устройств", "Приоритетная база", "FakeDNS Защита"],
|
||||
},
|
||||
MAINFRAME: {
|
||||
tag: "Командный Центр",
|
||||
desc: "Ультимативное решение для команд и независимости.",
|
||||
features: ["Безлимит устройств", "High-Load сервера", "Поддержка 24/7"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const botUsername = import.meta.env.VITE_BOT_USERNAME || "netrunner_vpn_bot";
|
||||
export default function Profile() {
|
||||
const [data, setData] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null); // <-- ДОБАВЛЕНО
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [showPlans, setShowPlans] = useState(false);
|
||||
|
||||
const [lang, setLang] = useState<"en" | "ru">(
|
||||
(localStorage.getItem("lang") as "en" | "ru") || "en",
|
||||
);
|
||||
const t = DICT[lang];
|
||||
|
||||
const botUsername = import.meta.env.VITE_BOT_USERNAME || "";
|
||||
|
||||
useEffect(() => {
|
||||
fetchProfile()
|
||||
.then(setUser)
|
||||
.catch(console.error)
|
||||
.then((res) => {
|
||||
setData(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
setError(err.message || "Failed to connect to Netrunner Core"); // <-- ДОБАВЛЕНО
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const toggleLang = () => {
|
||||
const next = lang === "en" ? "ru" : "en";
|
||||
setLang(next);
|
||||
localStorage.setItem("lang", next);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center text-primary">
|
||||
<Loader2 className="w-12 h-12 animate-spin mb-4" />
|
||||
<p className="tracking-[0.3em] uppercase text-sm animate-pulse">
|
||||
Decrypting_Profile...
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-[#05050A]">
|
||||
<Loader2 className="w-12 h-12 animate-spin mb-4 text-secondary drop-shadow-[0_0_15px_rgba(0,229,242,0.5)]" />
|
||||
<p className="tracking-[0.3em] uppercase text-sm animate-pulse text-secondary">
|
||||
Authenticating...
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) return null;
|
||||
// <-- ДОБАВЛЕН ЭКРАН ОШИБКИ ВМЕСТО return null -->
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center bg-[#05050A] text-red-500 font-mono p-6 text-center">
|
||||
<Shield className="w-16 h-16 mb-4 text-red-600 opacity-80" />
|
||||
<h1 className="text-2xl font-black tracking-widest mb-2">
|
||||
ACCESS DENIED
|
||||
</h1>
|
||||
<p className="text-sm opacity-70 mb-6">
|
||||
{error || "No data received from core."}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Пожалуйста, откройте терминал через интерфейс Телеграм-бота.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// БЕЗОПАСНОЕ ИЗВЛЕЧЕНИЕ: Если чего-то нет, не падаем, а ставим дефолт
|
||||
// Твой бэкенд отдает Json(user), так что data - это и есть объект пользователя
|
||||
const user = data?.user || data || {};
|
||||
const subscription = data?.subscription || {
|
||||
plan_name: "Free",
|
||||
expires_at: null,
|
||||
};
|
||||
const recruits = data?.recruits || 0;
|
||||
const earned = data?.earned || 0;
|
||||
const plans = data?.plans || [
|
||||
{ name: "GHOST", price: 5 },
|
||||
{ name: "NETRUNNER", price: 9 },
|
||||
{ name: "MAINFRAME", price: 20 },
|
||||
];
|
||||
|
||||
const copyRef = () => {
|
||||
navigator.clipboard.writeText(
|
||||
`https://t.me/netrunner_vpn_bot?start=${user.tg_id}`,
|
||||
`https://t.me/${botUsername}?start=${user.tg_id || user.id}`,
|
||||
);
|
||||
alert("Copied to clipboard!");
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen p-4 sm:p-6 max-w-2xl mx-auto pb-20">
|
||||
<header className="mb-8 text-center pt-8">
|
||||
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full border border-primary/30 bg-primary/10 text-primary text-xs font-bold uppercase tracking-widest mb-4">
|
||||
<Shield className="w-4 h-4" /> Secure Session Active
|
||||
<div className="min-h-screen bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-[#131124] via-[#08080C] to-black p-4 sm:p-6 pb-24 text-foreground font-mono">
|
||||
<header className="mb-8 text-center pt-8 relative z-10 flex flex-col items-center">
|
||||
<button
|
||||
onClick={toggleLang}
|
||||
className="absolute top-4 right-4 bg-white/5 border border-white/10 p-2 rounded-full text-muted-foreground hover:text-white transition-colors"
|
||||
>
|
||||
<Globe className="w-4 h-4" />
|
||||
</button>
|
||||
<div className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full border border-secondary/30 bg-secondary/5 text-secondary text-xs font-bold uppercase tracking-widest mb-4 shadow-[0_0_15px_rgba(0,229,242,0.1)]">
|
||||
<Shield className="w-4 h-4" /> {t.auth_ready}
|
||||
</div>
|
||||
<h1 className="text-3xl font-black tracking-tight">DATA VAULT</h1>
|
||||
<h1 className="text-3xl font-black tracking-tight drop-shadow-md">
|
||||
NETRUNNER_OS
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="grid gap-6"
|
||||
className="grid gap-6 max-w-2xl mx-auto relative z-10"
|
||||
>
|
||||
{/* Balance & Status Card */}
|
||||
<div className="bg-card/40 backdrop-blur-xl border border-border/50 rounded-3xl p-6 shadow-lg">
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
{/* PROFILE & BALANCE */}
|
||||
<div className="bg-white/[0.03] backdrop-blur-2xl border border-white/10 rounded-3xl p-6 shadow-[0_8px_32px_0_rgba(0,0,0,0.4)] relative overflow-hidden">
|
||||
<div className="absolute -top-20 -right-20 w-64 h-64 bg-primary/20 rounded-full blur-[80px] pointer-events-none" />
|
||||
|
||||
<div className="flex justify-between items-start mb-8">
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs uppercase tracking-widest mb-1">
|
||||
Operative ID
|
||||
{t.operative}
|
||||
</p>
|
||||
<p className="text-sm font-bold opacity-80">
|
||||
{user.id.split("-")[0]}...{user.id.split("-")[4]}
|
||||
<p className="text-sm font-bold opacity-80 text-primary">
|
||||
@{user.tg_username || user.tg_id || "Anon"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-muted-foreground text-xs uppercase tracking-widest mb-1">
|
||||
Balance
|
||||
{t.balance}
|
||||
</p>
|
||||
<p className="text-2xl font-black text-secondary drop-shadow-[0_0_10px_rgba(0,229,242,0.5)]">
|
||||
{user.balance} <span className="text-sm">NRXP</span>
|
||||
<p className="text-3xl font-black text-secondary drop-shadow-[0_0_12px_rgba(0,229,242,0.6)]">
|
||||
{user.balance || 0}{" "}
|
||||
<span className="text-sm opacity-70">E$</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-black/40 border border-white/5 rounded-2xl p-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="w-4 h-4 text-primary" />
|
||||
<span className="text-sm uppercase tracking-widest text-muted-foreground">
|
||||
{t.uplink_status}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
className={`px-3 py-1 rounded-full text-xs font-bold uppercase ${subscription.plan_name === "Free" ? "bg-white/10 text-white" : "bg-primary/20 text-primary border border-primary/30 shadow-[0_0_10px_rgba(139,61,255,0.3)]"}`}
|
||||
>
|
||||
{subscription.plan_name} TIER
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{subscription.expires_at
|
||||
? `${t.expires}: ${new Date(subscription.expires_at).toLocaleDateString()}`
|
||||
: t.no_limit}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subscription & Keys */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<button className="bg-primary/10 border border-primary/30 hover:bg-primary/20 transition-colors rounded-3xl p-6 flex flex-col items-center justify-center text-center gap-3 group">
|
||||
<CreditCard className="w-8 h-8 text-primary group-hover:scale-110 transition-transform" />
|
||||
<div>
|
||||
<p className="font-bold text-sm">Uplink Status</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Manage Subscription
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
{/* SUBSCRIPTIONS */}
|
||||
<div className="bg-white/[0.03] backdrop-blur-xl border border-white/10 rounded-3xl p-6 shadow-[0_8px_32px_0_rgba(0,0,0,0.3)]">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="font-bold text-lg">{t.system_override}</h3>
|
||||
<button
|
||||
onClick={() => setShowPlans(!showPlans)}
|
||||
className="text-xs flex items-center gap-1 bg-secondary/10 hover:bg-secondary/20 text-secondary border border-secondary/30 px-3 py-1.5 rounded-full transition-colors"
|
||||
>
|
||||
<ArrowUpCircle className="w-4 h-4" /> {t.manage_plan}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button className="bg-card/40 border border-border/50 hover:border-secondary/50 transition-colors rounded-3xl p-6 flex flex-col items-center justify-center text-center gap-3 group">
|
||||
<Key className="w-8 h-8 text-secondary group-hover:scale-110 transition-transform" />
|
||||
<div>
|
||||
<p className="font-bold text-sm">Access Keys</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
Download Configs
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
<AnimatePresence>
|
||||
{showPlans && (
|
||||
<motion.div
|
||||
initial={{ height: 0, opacity: 0 }}
|
||||
animate={{ height: "auto", opacity: 1 }}
|
||||
exit={{ height: 0, opacity: 0 }}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="grid sm:grid-cols-1 gap-4 mt-4 pb-2">
|
||||
{plans.map((plan: any) => {
|
||||
const planData = t.plans[
|
||||
plan.name as keyof typeof t.plans
|
||||
] || {
|
||||
tag: "Custom Tier",
|
||||
desc: "System expansion.",
|
||||
features: [],
|
||||
};
|
||||
return (
|
||||
<div
|
||||
key={plan.name}
|
||||
className="bg-black/50 border border-white/10 hover:border-primary/50 rounded-2xl p-4 flex flex-col transition-colors group relative overflow-hidden"
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<h4 className="font-bold text-primary uppercase text-lg">
|
||||
{plan.name}
|
||||
</h4>
|
||||
<p className="text-[10px] uppercase tracking-widest text-secondary">
|
||||
{planData.tag}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-2xl font-black text-white">
|
||||
{plan.price}{" "}
|
||||
<span className="text-xs text-muted-foreground font-normal">
|
||||
E$/mo
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mb-3 relative z-10">
|
||||
{planData.desc}
|
||||
</p>
|
||||
<ul className="text-xs text-muted-foreground space-y-1 mb-4 relative z-10">
|
||||
{planData.features.map((f: string, i: number) => (
|
||||
<li key={i}>- {f}</li>
|
||||
))}
|
||||
</ul>
|
||||
<button className="mt-auto w-full py-2 rounded-xl bg-white/5 group-hover:bg-primary text-xs font-bold transition-colors relative z-10">
|
||||
{t.acquire}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
|
||||
{/* Referral Program */}
|
||||
<div className="bg-card/40 backdrop-blur-xl border border-border/50 rounded-3xl p-6 relative overflow-hidden group">
|
||||
<Zap className="absolute -right-4 -bottom-4 w-32 h-32 text-secondary opacity-5 group-hover:opacity-10 transition-opacity" />
|
||||
<h3 className="font-bold text-lg mb-2">Referral Network</h3>
|
||||
<p className="text-sm text-muted-foreground mb-4 max-w-[80%]">
|
||||
Invite operatives to the network. Earn 100 NRXP for each successful
|
||||
uplink initialization.
|
||||
{/* SYNDICATE NETWORK */}
|
||||
<div className="bg-white/[0.03] backdrop-blur-xl border border-white/10 rounded-3xl p-6 relative overflow-hidden group">
|
||||
<Zap className="absolute -right-4 -bottom-4 w-32 h-32 text-secondary opacity-5 group-hover:opacity-10 transition-opacity blur-[2px]" />
|
||||
<h3 className="font-bold text-lg mb-4 relative z-10 flex items-center gap-2">
|
||||
<Users className="w-5 h-5 text-secondary" /> {t.syndicate}
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4 mb-6 relative z-10">
|
||||
<div className="bg-black/40 border border-white/5 rounded-2xl p-4 text-center">
|
||||
<p className="text-2xl font-black text-white">{recruits}</p>
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mt-1">
|
||||
{t.recruits}
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-black/40 border border-white/5 rounded-2xl p-4 text-center">
|
||||
<p className="text-2xl font-black text-secondary">{earned}</p>
|
||||
<p className="text-[10px] uppercase tracking-widest text-muted-foreground mt-1">
|
||||
{t.earned}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mb-4 max-w-[90%] relative z-10">
|
||||
{t.ref_desc}
|
||||
</p>
|
||||
<button
|
||||
onClick={copyRef}
|
||||
className="w-full bg-background border border-border/50 hover:border-secondary/50 flex items-center justify-between p-4 rounded-2xl transition-colors"
|
||||
className="w-full relative z-10 bg-black/60 backdrop-blur-md border border-white/10 hover:border-secondary/50 flex items-center justify-between p-4 rounded-2xl transition-all"
|
||||
>
|
||||
<span className="text-xs text-muted-foreground truncate mr-4">
|
||||
https://t.me/{botUsername}?start={user.tg_id}
|
||||
<span className="text-xs text-muted-foreground truncate mr-4 transition-colors">
|
||||
t.me/{botUsername}?start={user.tg_id || user.id}
|
||||
</span>
|
||||
<Copy className="w-4 h-4 text-secondary shrink-0" />
|
||||
{copied ? (
|
||||
<CheckCircle2 className="w-4 h-4 text-secondary shrink-0" />
|
||||
) : (
|
||||
<Copy className="w-4 h-4 text-secondary shrink-0" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
Reference in New Issue
Block a user