trial updates

This commit is contained in:
2026-04-24 15:51:03 +07:00
parent 808c07855c
commit 24e3a6de44
4 changed files with 126 additions and 82 deletions
+57 -52
View File
@@ -14,7 +14,7 @@ import {
} from "lucide-react";
import { activateTrial, buySubscription, fetchProfile } from "./api";
import { useNavigate } from "react-router-dom";
// --- ЛОКАЛИЗАЦИЯ И СТАТИКА ---
const DICT = {
en: {
auth_ready: "App Authorization Ready",
@@ -22,16 +22,22 @@ const DICT = {
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",
activate_trial: "Activate Trial",
trial_used: "Trial Exhausted",
syndicate: "Syndicate Network",
recruits: "Recruits",
earned: "Eddies Earned",
ref_desc: "Expand the grid. Earn 100 E$ per initialized uplink.",
copied: "Copied!",
plans: {
TRIAL: {
tag: "Free Trial",
desc: "Experience the Grid. 3 days of unrestricted access.",
features: ["5GB Bandwidth", "No logs", "Standard Nodes"],
},
GHOST: {
tag: "Personal Uplink",
desc: "The ideal choice for personal use. Minimal digital footprint.",
@@ -59,16 +65,22 @@ const DICT = {
balance: "Баланс",
uplink_status: "Статус Узла",
expires: "Истекает",
no_limit: "Скорость ограничена. Повысьте уровень доступа.",
system_override: "Терминал Доступа",
manage_plan: "Управление подпиской",
acquire: "Арендовать",
activate_trial: "Начать Триал",
trial_used: "Триал Использован",
syndicate: "Синдикат",
recruits: "Рекруты",
earned: "Заработано (E$)",
ref_desc: "Расширяй сеть. Получай 100 E$ за каждый новый узел.",
copied: "Скопировано!",
plans: {
TRIAL: {
tag: "Бесплатный Тест",
desc: "Испытай Сеть. 3 дня безлимитного доступа.",
features: ["5GB Трафика", "Без логов", "Стандартные узлы"],
},
GHOST: {
tag: "Личный Узел",
desc: "Идеально для себя. Минимальный цифровой след.",
@@ -88,10 +100,18 @@ const DICT = {
},
};
// Захардкодим список, чтобы Триал был виден красиво
const UI_PLANS = [
{ name: "TRIAL", price: 0, price_usd: 0, price_rub: 0 },
{ name: "GHOST", price: 5, price_usd: 5, price_rub: 500 },
{ name: "NETRUNNER", price: 9, price_usd: 9, price_rub: 900 },
{ name: "MAINFRAME", price: 20, price_usd: 20, price_rub: 2000 },
];
export default function Profile() {
const [data, setData] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); // <-- ДОБАВЛЕНО
const [error, setError] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const [showPlans, setShowPlans] = useState(false);
const navigate = useNavigate();
@@ -100,17 +120,14 @@ export default function Profile() {
(localStorage.getItem("lang") as "en" | "ru") || "en",
);
const t = DICT[lang];
const botUsername = import.meta.env.VITE_BOT_USERNAME || "";
useEffect(() => {
fetchProfile()
.then((res) => {
setData(res);
})
.then((res) => setData(res))
.catch((err) => {
console.error(err);
setError(err.message || "Failed to connect to Netrunner Core"); // <-- ДОБАВЛЕНО
setError(err.message || "Failed to connect to Netrunner Core");
})
.finally(() => setLoading(false));
}, []);
@@ -132,7 +149,6 @@ export default function Profile() {
);
}
// <-- ДОБАВЛЕН ЭКРАН ОШИБКИ ВМЕСТО 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">
@@ -150,18 +166,12 @@ export default function Profile() {
);
}
// БЕЗОПАСНОЕ ИЗВЛЕЧЕНИЕ: Если чего-то нет, не падаем, а ставим дефолт
// Твой бэкенд отдает Json(user), так что data - это и есть объект пользователя
const user = data?.user || data || {};
const subscription = data?.subscription; // Теперь может быть null
// Теперь бэкенд отдает всё как надо: { user: {...}, subscription: {...}, recruits, earned }
const user = data.user || {};
const subscription = data.subscription;
const hasSub = subscription && subscription.plan_name;
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 recruits = data.recruits || 0;
const earned = data.earned || 0;
const copyRef = () => {
navigator.clipboard.writeText(
@@ -172,6 +182,7 @@ export default function Profile() {
};
const getPrice = (usd: number, rub: number) => {
if (usd === 0) return "FREE";
return lang === "ru" ? `${rub} ₽/мес` : `$${usd}/mo`;
};
@@ -259,19 +270,9 @@ export default function Profile() {
{new Date(subscription.expires_at).toLocaleDateString()}
</p>
) : (
<div className="flex justify-between items-center mt-2">
<p className="text-xs text-muted-foreground">
No active subscription.
</p>
{!user.has_used_trial && (
<button
onClick={handleTrial}
className="text-xs bg-secondary/20 hover:bg-secondary/40 text-secondary px-3 py-1 rounded transition-colors"
>
ACTIVATE 3-DAY TRIAL (5GB)
</button>
)}
</div>
<p className="text-xs text-muted-foreground mt-2">
No active subscription.
</p>
)}
</div>
@@ -283,6 +284,7 @@ export default function Profile() {
<Terminal className="w-5 h-5 group-hover:animate-pulse relative z-10" />
<span className="relative z-10">Run Breach Protocol</span>
</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">
@@ -304,18 +306,17 @@ export default function Profile() {
className="overflow-hidden"
>
<div className="grid sm:grid-cols-1 gap-4 mt-4 pb-2">
{plans.map((plan: any) => {
{UI_PLANS.map((plan: any) => {
const planData = t.plans[
plan.name as keyof typeof t.plans
] || {
tag: "Custom Tier",
desc: "System expansion.",
features: [],
};
] || { tag: "", desc: "", features: [] };
const isTrial = plan.name === "TRIAL";
const isTrialUsed = isTrial && user.has_used_trial;
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"
className={`bg-black/50 border border-white/10 rounded-2xl p-4 flex flex-col transition-colors group relative overflow-hidden ${isTrialUsed ? "opacity-50 grayscale" : "hover:border-primary/50"}`}
>
<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">
@@ -328,10 +329,10 @@ export default function Profile() {
</p>
</div>
<p className="text-2xl font-black text-white">
{plan.price}{" "}
<span className="text-xs text-muted-foreground font-normal">
E$/mo
</span>
{getPrice(
plan.price_usd || plan.price,
plan.price_rub || plan.price * 100,
)}
</p>
</div>
<p className="text-xs text-muted-foreground mb-3 relative z-10">
@@ -343,14 +344,18 @@ export default function Profile() {
))}
</ul>
<button
onClick={() => handleBuy(plan.name)}
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"
onClick={() =>
isTrial ? handleTrial() : handleBuy(plan.name)
}
disabled={isTrialUsed}
className={`mt-auto w-full py-2 rounded-xl text-xs font-bold transition-colors relative z-10
${isTrialUsed ? "bg-white/5 text-muted-foreground cursor-not-allowed" : "bg-white/5 hover:bg-primary text-white"}`}
>
{t.acquire}{" "}
{getPrice(
plan.price_usd || plan.price,
plan.price_rub || plan.price * 100,
)}
{isTrial
? isTrialUsed
? t.trial_used
: t.activate_trial
: `${t.acquire}`}
</button>
</div>
);