This commit is contained in:
gpatruno
2026-07-18 16:26:08 +02:00
parent 49c444e8b9
commit 2614d679c8
85 changed files with 8074 additions and 710 deletions
+199
View File
@@ -0,0 +1,199 @@
import { useEffect, useState } from 'react';
import { Navigate } from 'react-router-dom';
import { api, formatCredits } from '../api';
import { useAuth } from '../AuthContext';
const CHOICE_LABELS = {
chance: 'Chance',
gain: 'Gain',
key: 'Key progress',
};
export default function PrestigePage() {
const { user, loading, patchUser, refresh } = useAuth();
const [data, setData] = useState(null);
const [selected, setSelected] = useState(null);
const [confirmText, setConfirmText] = useState('');
const [error, setError] = useState('');
const [message, setMessage] = useState('');
const [busy, setBusy] = useState(false);
const load = async () => {
const result = await api.prestige();
setData(result);
};
useEffect(() => {
if (!user || user.role === 'admin') return undefined;
let cancelled = false;
(async () => {
try {
const result = await api.prestige();
if (!cancelled) setData(result);
} catch (err) {
if (!cancelled) setError(err.message);
}
})();
return () => {
cancelled = true;
};
}, [user?.id, user?.role]);
if (loading) return <p className="muted">Loading</p>;
if (!user) return <Navigate to="/login" replace />;
if (user.role === 'admin') return <Navigate to="/admin" replace />;
if (!data && !error) return <p className="muted">Loading</p>;
if (data && data.pageVisible === false) {
return <Navigate to="/dashboard" replace />;
}
const cost = data?.prestige?.costCents ?? 0;
const p = data?.prestige;
const unlockAt = data?.prestige?.visibleAtCents ?? 0;
const onPrestige = async () => {
if (!selected || confirmText !== 'PRESTIGE') return;
setBusy(true);
setError('');
setMessage('');
try {
const result = await api.doPrestige(selected);
patchUser(result.user);
await refresh();
setMessage(
`Prestige #${result.prestige.count} complete · chose ${CHOICE_LABELS[result.choice] || result.choice}. Account reset.`
);
setSelected(null);
setConfirmText('');
await load();
} catch (err) {
setError(err.message);
} finally {
setBusy(false);
}
};
return (
<section className="section">
<div className="section-head">
<div>
<h1>Prestige</h1>
<p className="muted">
Pay {formatCredits(cost)} cr, pick one permanent boost, then reset balance, inventory,
keys, and catalog. History is kept. Unlocks at {formatCredits(unlockAt)} cr total wealth.
</p>
</div>
{p && (
<div className="prestige-count-pill">
Prestige <strong>{p.count}</strong>
</div>
)}
</div>
{error && <div className="error">{error}</div>}
{message && <div className="ok-msg">{message}</div>}
{p && (
<div className="prestige-totals">
<div>
<span className="muted">Chance</span>
<strong>+{p.chanceBonus}%</strong>
</div>
<div>
<span className="muted">Gains</span>
<strong>+{p.gainBonus}%</strong>
</div>
<div>
<span className="muted">Key opens</span>
<strong>{p.keyOpenReduction}</strong>
</div>
</div>
)}
<div className="prestige-cost-row">
<span className="muted">Cost</span>
<strong className="gold">{formatCredits(cost)} cr</strong>
<span className="muted">Your balance</span>
<strong>{formatCredits(user.balance)} cr</strong>
</div>
{!data?.canAfford && (
<p className="muted">You need {formatCredits(cost)} cr to prestige.</p>
)}
{data?.lockedItems > 0 && (
<div className="error">
Withdraw {data.lockedItems} locked item{data.lockedItems === 1 ? '' : 's'} from battles
before prestiging.
</div>
)}
<div className="prestige-choices">
{(data?.choices || []).map((c) => (
<button
key={c.id}
type="button"
className={`prestige-choice${selected === c.id ? ' selected' : ''}`}
onClick={() => setSelected(c.id)}
disabled={busy || !data?.canPrestige}
>
<h2>{c.title}</h2>
<p className="muted">{c.description}</p>
<div className="prestige-choice-meta">
<span>
Now <strong>+{c.current}{c.id === 'key' ? '' : '%'}</strong>
{c.id === 'key' ? ' opens reduced' : ''}
</span>
<span className="prestige-delta">
+{c.current + c.delta}
{c.id === 'key' ? '' : '%'}
</span>
</div>
</button>
))}
</div>
<div className="prestige-confirm admin-block">
<p>
This permanently spends {formatCredits(cost)} cr and wipes your balance, inventory, case
keys, and catalog. Type <strong>PRESTIGE</strong> to confirm.
</p>
<label>
Confirmation
<input
value={confirmText}
onChange={(e) => setConfirmText(e.target.value)}
placeholder="PRESTIGE"
disabled={busy || !selected}
autoComplete="off"
/>
</label>
<button
type="button"
className="btn"
disabled={
busy || !data?.canPrestige || !selected || confirmText !== 'PRESTIGE'
}
onClick={onPrestige}
>
{busy ? 'Prestiging…' : 'Prestige now'}
</button>
</div>
{data?.history?.length > 0 && (
<div className="prestige-history">
<h2>History</h2>
<ul className="stat-list">
{data.history.map((h) => (
<li key={h.id}>
{CHOICE_LABELS[h.choice] || h.choice}{' '}
<span className="muted">
· {new Date(h.createdAt).toLocaleString()}
</span>
</li>
))}
</ul>
</div>
)}
</section>
);
}