trial log, profile fixes
This commit is contained in:
@@ -4,7 +4,6 @@ import { Cyberhack, type CyberhackResult } from "cyberhack";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import { submitHackResult } from "./api";
|
||||
|
||||
// --- ЛОКАЛИЗАЦИЯ ПРАВИЛ ---
|
||||
const DICT = {
|
||||
en: {
|
||||
title: "SIMULATION MODE WARNING",
|
||||
@@ -20,42 +19,30 @@ const DICT = {
|
||||
|
||||
export default function HackGame() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// ИСПРАВЛЕНО: Читаем язык синхронно до первого рендера WASM
|
||||
const [lang] = useState<"en" | "ru">(
|
||||
() => (localStorage.getItem("lang") as "en" | "ru") || "en",
|
||||
);
|
||||
const t = DICT[lang];
|
||||
|
||||
// Стейт для отображения правил
|
||||
const [showRules, setShowRules] = useState(true);
|
||||
|
||||
const handleComplete = async (result: CyberhackResult) => {
|
||||
console.log("[BREACH_PROTOCOL] Terminated. Output:", result);
|
||||
|
||||
try {
|
||||
const earnedPoints = result.total_coins || 0;
|
||||
|
||||
if (earnedPoints > 0) {
|
||||
// Бэкенд сам решит, начислять или нет, в зависимости от наличия тикетов
|
||||
await submitHackResult(earnedPoints);
|
||||
console.log(
|
||||
`[BREACH_PROTOCOL] ${earnedPoints} E$ synced with Netrunner Core.`,
|
||||
);
|
||||
} else {
|
||||
console.log("[BREACH_PROTOCOL] Hack failed or aborted.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("[ERROR] Failed to sync data:", err);
|
||||
}
|
||||
|
||||
// Принудительный редирект в Личный Кабинет
|
||||
window.location.href = "https://account.netrunner-vpn.com";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 bg-[#05050A] flex items-center justify-center font-mono">
|
||||
{/* Кнопка экстренной отмены */}
|
||||
<div className="fixed inset-0 z-50 bg-[#05050A] font-mono">
|
||||
<button
|
||||
onClick={() => navigate("/profile")}
|
||||
className="absolute top-4 right-4 text-muted-foreground hover:text-[#FF003C] z-[60] text-xs uppercase tracking-widest border border-white/10 hover:border-[#FF003C]/50 px-3 py-1 rounded transition-colors bg-black/50 backdrop-blur-sm"
|
||||
@@ -63,24 +50,19 @@ export default function HackGame() {
|
||||
[Abort Sequence]
|
||||
</button>
|
||||
|
||||
{/* ОВЕРЛЕЙ С ПРАВИЛАМИ */}
|
||||
{showRules && (
|
||||
<div className="absolute inset-0 z-[70] bg-black/80 backdrop-blur-md flex items-center justify-center p-4">
|
||||
<div className="bg-[#0B0D17] border border-primary/50 rounded-2xl p-6 sm:p-8 max-w-md w-full shadow-[0_0_40px_rgba(139,61,255,0.15)] relative overflow-hidden">
|
||||
{/* Декоративный глитч-фон */}
|
||||
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-primary to-transparent opacity-50" />
|
||||
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<AlertTriangle className="w-6 h-6 text-primary animate-pulse" />
|
||||
<h2 className="text-primary font-black text-lg sm:text-xl tracking-widest uppercase">
|
||||
{t.title}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground text-xs sm:text-sm mb-8 leading-relaxed opacity-90">
|
||||
{t.text}
|
||||
</p>
|
||||
|
||||
<button
|
||||
onClick={() => setShowRules(false)}
|
||||
className="w-full bg-primary/10 hover:bg-primary/30 text-primary border border-primary/50 text-xs sm:text-sm font-bold uppercase tracking-widest py-3 rounded-xl transition-all shadow-[0_0_15px_rgba(139,61,255,0.2)] hover:shadow-[0_0_25px_rgba(139,61,255,0.4)]"
|
||||
@@ -91,24 +73,9 @@ export default function HackGame() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Обертка для проброса CSS-переменных.
|
||||
Рендерим Cyberhack только если правила приняты, чтобы таймер игры
|
||||
не начал тикать под модальным окном.
|
||||
*/}
|
||||
{/* 🔥 ИСПРАВЛЕНО: Убрал flex centering, поставил жесткий absolute inset-0 block */}
|
||||
{!showRules && (
|
||||
<div
|
||||
className="w-full h-full flex items-center justify-center"
|
||||
style={{
|
||||
// @ts-expect-error - Переопределение кастомных свойств библиотеки
|
||||
"--ch-primary": "#8B3DFF", // Netrunner Purple
|
||||
"--ch-secondary": "#00E5F2", // Netrunner Cyan
|
||||
"--ch-background": "#05050A", // Deep Void (сделал чуть темнее для контраста)
|
||||
"--ch-surface": "#16192B", // Карточки
|
||||
"--ch-text": "#F8FAFC", // Белый текст
|
||||
"--ch-error": "#FF003C", // Cyberpunk Red для ошибок
|
||||
"--ch-success": "#00FF66", // Neon Green для успеха
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 w-full h-full block bg-[#05050A]">
|
||||
<Cyberhack
|
||||
timeLimit={45}
|
||||
baseValue={50}
|
||||
|
||||
+41
-17
@@ -15,13 +15,18 @@ import {
|
||||
import { activateTrial, buySubscription, fetchProfile } from "./api";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
// 🔥 ИСПРАВЛЕНО: Полная локализация интерфейса
|
||||
const DICT = {
|
||||
en: {
|
||||
auth_ready: "App Authorization Ready",
|
||||
operative: "Operative",
|
||||
balance: "Balance",
|
||||
tickets: "Tickets",
|
||||
uplink_status: "Uplink Status",
|
||||
expires: "Expires",
|
||||
offline: "OFFLINE",
|
||||
no_active_sub: "No active subscription.",
|
||||
run_breach: "Run Breach Protocol",
|
||||
system_override: "System Override",
|
||||
manage_plan: "Manage Plan",
|
||||
acquire: "Acquire Tier",
|
||||
@@ -63,8 +68,12 @@ const DICT = {
|
||||
auth_ready: "Авторизация Успешна",
|
||||
operative: "Оперативник",
|
||||
balance: "Баланс",
|
||||
tickets: "Билеты",
|
||||
uplink_status: "Статус Узла",
|
||||
expires: "Истекает",
|
||||
offline: "ОТКЛЮЧЕНО",
|
||||
no_active_sub: "Нет активной подписки.",
|
||||
run_breach: "Запустить Протокол Взлома",
|
||||
system_override: "Терминал Доступа",
|
||||
manage_plan: "Управление подпиской",
|
||||
acquire: "Арендовать",
|
||||
@@ -100,7 +109,6 @@ 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 },
|
||||
@@ -166,7 +174,6 @@ export default function Profile() {
|
||||
);
|
||||
}
|
||||
|
||||
// Теперь бэкенд отдает всё как надо: { user: {...}, subscription: {...}, recruits, earned }
|
||||
const user = data.user || {};
|
||||
const subscription = data.subscription;
|
||||
const hasSub = subscription && subscription.plan_name;
|
||||
@@ -190,8 +197,9 @@ export default function Profile() {
|
||||
try {
|
||||
await activateTrial();
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
alert("Error / Уже использовано");
|
||||
} catch (e: any) {
|
||||
// 🔥 ИСПРАВЛЕНО: Вывод реальной ошибки с бэкенда
|
||||
alert(`[ERROR] ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -199,8 +207,8 @@ export default function Profile() {
|
||||
try {
|
||||
await buySubscription(planName);
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
alert("Error");
|
||||
} catch (e: any) {
|
||||
alert(`[ERROR] ${e.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -237,7 +245,7 @@ export default function Profile() {
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-muted-foreground text-xs uppercase tracking-widest mb-1">
|
||||
{t.balance} | TICKETS:{" "}
|
||||
{t.balance} | {t.tickets}:{" "}
|
||||
<span className="text-primary">{user.game_tickets || 0}</span>
|
||||
</p>
|
||||
<p className="text-3xl font-black text-secondary drop-shadow-[0_0_12px_rgba(0,229,242,0.6)]">
|
||||
@@ -260,7 +268,7 @@ export default function Profile() {
|
||||
</span>
|
||||
) : (
|
||||
<span className="px-3 py-1 rounded-full text-xs font-bold uppercase bg-white/10 text-muted-foreground">
|
||||
OFFLINE
|
||||
{t.offline}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -271,7 +279,7 @@ export default function Profile() {
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
No active subscription.
|
||||
{t.no_active_sub}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -282,7 +290,7 @@ export default function Profile() {
|
||||
>
|
||||
<div className="absolute inset-0 bg-[linear-gradient(45deg,transparent_25%,rgba(139,61,255,0.2)_50%,transparent_75%)] bg-[length:250%_250%,100%_100%] animate-[shimmer_2s_infinite] opacity-0 group-hover:opacity-100" />
|
||||
<Terminal className="w-5 h-5 group-hover:animate-pulse relative z-10" />
|
||||
<span className="relative z-10">Run Breach Protocol</span>
|
||||
<span className="relative z-10">{t.run_breach}</span>
|
||||
</button>
|
||||
|
||||
{/* SUBSCRIPTIONS */}
|
||||
@@ -313,10 +321,17 @@ export default function Profile() {
|
||||
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 rounded-2xl p-4 flex flex-col transition-colors group relative overflow-hidden ${isTrialUsed ? "opacity-50 grayscale" : "hover:border-primary/50"}`}
|
||||
className={`bg-black/50 border rounded-3xl p-5 flex flex-col transition-all duration-300 group relative overflow-hidden ${
|
||||
isTrial
|
||||
? isTrialUsed
|
||||
? "border-white/5 opacity-50 grayscale"
|
||||
: "border-secondary/50 shadow-[0_0_15px_rgba(0,229,242,0.15)] hover:shadow-[0_0_25px_rgba(0,229,242,0.3)] hover:border-secondary"
|
||||
: "border-white/10 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">
|
||||
@@ -335,12 +350,15 @@ export default function Profile() {
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mb-3 relative z-10">
|
||||
<p className="text-xs text-muted-foreground mb-4 relative z-10">
|
||||
{planData.desc}
|
||||
</p>
|
||||
<ul className="text-xs text-muted-foreground space-y-1 mb-4 relative z-10">
|
||||
<ul className="text-xs text-muted-foreground space-y-2 mb-6 relative z-10">
|
||||
{planData.features.map((f: string, i: number) => (
|
||||
<li key={i}>- {f}</li>
|
||||
<li key={i} className="flex items-center gap-2">
|
||||
<div className="w-1 h-1 bg-secondary rounded-full" />{" "}
|
||||
{f}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<button
|
||||
@@ -348,14 +366,20 @@ export default function Profile() {
|
||||
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"}`}
|
||||
className={`mt-auto w-full py-3 rounded-2xl text-xs font-bold uppercase tracking-widest transition-colors relative z-10
|
||||
${
|
||||
isTrialUsed
|
||||
? "bg-white/5 text-muted-foreground cursor-not-allowed"
|
||||
: isTrial
|
||||
? "bg-secondary/20 hover:bg-secondary/40 text-secondary border border-secondary/50"
|
||||
: "bg-white/5 hover:bg-primary text-white border border-white/10 hover:border-primary"
|
||||
}`}
|
||||
>
|
||||
{isTrial
|
||||
? isTrialUsed
|
||||
? t.trial_used
|
||||
: t.activate_trial
|
||||
: `${t.acquire}`}
|
||||
: t.acquire}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
+10
-7
@@ -7,17 +7,13 @@ export const fetchProfile = async () => {
|
||||
if (!token) throw new Error("No token found");
|
||||
|
||||
const res = await fetch(`${API_BASE}/me`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!res.ok) throw new Error("Failed to fetch profile");
|
||||
return res.json();
|
||||
};
|
||||
|
||||
// Существующий код...
|
||||
|
||||
export const submitHackResult = async (score: number) => {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
if (!token) throw new Error("No token found");
|
||||
@@ -35,13 +31,17 @@ export const submitHackResult = async (score: number) => {
|
||||
return res.json();
|
||||
};
|
||||
|
||||
// 🔥 ИСПРАВЛЕНО: Правильный парсинг ошибки Триала
|
||||
export const activateTrial = async () => {
|
||||
const token = localStorage.getItem(TOKEN_KEY);
|
||||
const res = await fetch(`${API_BASE}/trial`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
if (!res.ok) {
|
||||
const errData = await res.json().catch(() => ({}));
|
||||
throw new Error(errData.error || res.statusText || "Activation failed");
|
||||
}
|
||||
return res.json();
|
||||
};
|
||||
|
||||
@@ -55,6 +55,9 @@ export const buySubscription = async (plan: string) => {
|
||||
},
|
||||
body: JSON.stringify({ plan }),
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
if (!res.ok) {
|
||||
const errData = await res.json().catch(() => ({}));
|
||||
throw new Error(errData.error || res.statusText || "Purchase failed");
|
||||
}
|
||||
return res.json();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user