LoLStats: Champion Tier List pro Game Mode (ARAM, Arena, URF, etc.)
Neue op.gg Champion API (lol-api-champion.op.gg) fuer Tier-Daten. 30-Min In-Memory Cache pro Mode+Region. Neuer Bereich unterhalb der Match History mit Mode-Tabs, Champion-Grid, Tier-Badges und Filter. Arena-Mode zeigt Avg Placement statt KDA. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9093f12259
commit
905f156d47
5 changed files with 397 additions and 3 deletions
|
|
@ -9,6 +9,13 @@ interface LeagueStat { game_type: string; win: number | null; lose: number | nul
|
|||
interface LadderRank { rank: number | null; total: number | null; }
|
||||
interface ChampionStat { champion_name: string; id: number; play: number; win: number; lose: number; kill: number; death: number; assist: number; }
|
||||
interface MostChampions { game_type: string; play: number; win: number; lose: number; champion_stats: ChampionStat[]; }
|
||||
interface GameModeInfo { key: string; label: string; }
|
||||
interface ChampionTierEntry {
|
||||
champion_id: number; champion_name: string; tier: number; rank: number;
|
||||
win_rate: number | null; pick_rate: number; ban_rate: number | null;
|
||||
kda: number | null; play: number;
|
||||
average_placement?: number; first_place_rate?: number;
|
||||
}
|
||||
|
||||
interface SummonerProfile {
|
||||
game_name: string; tagline: string; level: number; profile_image_url: string;
|
||||
|
|
@ -114,15 +121,42 @@ export default function LolstatsTab({ data }: { data: any }) {
|
|||
const [renewing, setRenewing] = useState(false);
|
||||
const [lastUpdated, setLastUpdated] = useState<string | null>(null);
|
||||
|
||||
// Tier list state
|
||||
const [tierMode, setTierMode] = useState('aram');
|
||||
const [tierRegion, setTierRegion] = useState('EUW');
|
||||
const [tierList, setTierList] = useState<ChampionTierEntry[]>([]);
|
||||
const [tierLoading, setTierLoading] = useState(false);
|
||||
const [tierError, setTierError] = useState<string | null>(null);
|
||||
const [gameModes, setGameModes] = useState<GameModeInfo[]>([]);
|
||||
const [tierSearch, setTierSearch] = useState('');
|
||||
const [showAllTiers, setShowAllTiers] = useState(false);
|
||||
|
||||
const searchRef = useRef<HTMLInputElement>(null);
|
||||
const currentSearchRef = useRef<{ gameName: string; tagLine: string; region: string } | null>(null);
|
||||
|
||||
// Load regions
|
||||
// Load regions + modes
|
||||
useEffect(() => {
|
||||
fetch('/api/lolstats/regions').then(r => r.json()).then(setRegions).catch(() => {});
|
||||
fetch('/api/lolstats/recent').then(r => r.json()).then(setRecentSearches).catch(() => {});
|
||||
fetch('/api/lolstats/modes').then(r => r.json()).then(setGameModes).catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Load tier list when mode/region changes
|
||||
useEffect(() => {
|
||||
if (!tierMode) return;
|
||||
setTierLoading(true);
|
||||
setTierError(null);
|
||||
setShowAllTiers(false);
|
||||
fetch(`/api/lolstats/tierlist?mode=${tierMode}®ion=${tierRegion}`)
|
||||
.then(r => {
|
||||
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
||||
return r.json();
|
||||
})
|
||||
.then(data => setTierList(data.champions ?? []))
|
||||
.catch(e => setTierError(e.message))
|
||||
.finally(() => setTierLoading(false));
|
||||
}, [tierMode, tierRegion]);
|
||||
|
||||
// SSE data
|
||||
useEffect(() => {
|
||||
if (!data) return;
|
||||
|
|
@ -538,6 +572,101 @@ export default function LolstatsTab({ data }: { data: any }) {
|
|||
<p>Gib einen Summoner Name#Tag ein und wähle die Region</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Champion Tier List — always visible */}
|
||||
<div className="lol-tier-section">
|
||||
<div className="lol-section-title">Champion Tier List</div>
|
||||
|
||||
<div className="lol-tier-controls">
|
||||
<div className="lol-tier-modes">
|
||||
{gameModes.map(m => (
|
||||
<button
|
||||
key={m.key}
|
||||
className={`lol-tier-mode-btn ${tierMode === m.key ? 'active' : ''}`}
|
||||
onClick={() => setTierMode(m.key)}
|
||||
>
|
||||
{m.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<select
|
||||
className="lol-search-region"
|
||||
value={tierRegion}
|
||||
onChange={e => setTierRegion(e.target.value)}
|
||||
>
|
||||
{regions.map(r => <option key={r.code} value={r.code}>{r.code}</option>)}
|
||||
</select>
|
||||
<input
|
||||
className="lol-tier-filter"
|
||||
placeholder="Filter champion..."
|
||||
value={tierSearch}
|
||||
onChange={e => setTierSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{tierLoading && (
|
||||
<div className="lol-loading">
|
||||
<div className="lol-spinner" />
|
||||
Lade Tier List...
|
||||
</div>
|
||||
)}
|
||||
{tierError && <div className="lol-error">{tierError}</div>}
|
||||
|
||||
{!tierLoading && !tierError && tierList.length > 0 && (
|
||||
<>
|
||||
<div className="lol-tier-table">
|
||||
<div className="lol-tier-header">
|
||||
<span className="lol-tier-col-rank">#</span>
|
||||
<span className="lol-tier-col-champ">Champion</span>
|
||||
<span className="lol-tier-col-tier">Tier</span>
|
||||
<span className="lol-tier-col-wr">{tierMode === 'arena' ? 'Win' : 'Win %'}</span>
|
||||
<span className="lol-tier-col-pr">Pick %</span>
|
||||
{tierMode !== 'arena' && <span className="lol-tier-col-br">Ban %</span>}
|
||||
<span className="lol-tier-col-kda">{tierMode === 'arena' ? 'Avg Place' : 'KDA'}</span>
|
||||
</div>
|
||||
{tierList
|
||||
.filter(c => !tierSearch || c.champion_name.toLowerCase().includes(tierSearch.toLowerCase()))
|
||||
.slice(0, showAllTiers ? undefined : 50)
|
||||
.map(c => {
|
||||
const tierLabels = ['OP', '1', '2', '3', '4', '5'];
|
||||
return (
|
||||
<div key={c.champion_id} className={`lol-tier-row tier-${c.tier}`}>
|
||||
<span className="lol-tier-col-rank">{c.rank}</span>
|
||||
<span className="lol-tier-col-champ">
|
||||
<img src={champImg(c.champion_name)} alt={c.champion_name} />
|
||||
{c.champion_name}
|
||||
</span>
|
||||
<span className={`lol-tier-col-tier tier-badge-${c.tier}`}>
|
||||
{tierLabels[c.tier] ?? c.tier}
|
||||
</span>
|
||||
<span className="lol-tier-col-wr">
|
||||
{c.win_rate != null ? `${(c.win_rate * 100).toFixed(1)}%` : '-'}
|
||||
</span>
|
||||
<span className="lol-tier-col-pr">
|
||||
{(c.pick_rate * 100).toFixed(1)}%
|
||||
</span>
|
||||
{tierMode !== 'arena' && (
|
||||
<span className="lol-tier-col-br">
|
||||
{c.ban_rate != null ? `${(c.ban_rate * 100).toFixed(1)}%` : '-'}
|
||||
</span>
|
||||
)}
|
||||
<span className="lol-tier-col-kda">
|
||||
{tierMode === 'arena'
|
||||
? (c.average_placement?.toFixed(1) ?? '-')
|
||||
: (c.kda?.toFixed(2) ?? '-')}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{!showAllTiers && tierList.length > 50 && (
|
||||
<button className="lol-load-more" onClick={() => setShowAllTiers(true)}>
|
||||
Alle {tierList.length} Champions anzeigen
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -490,6 +490,133 @@
|
|||
}
|
||||
.lol-load-more:hover { border-color: var(--accent); color: var(--text-normal); }
|
||||
|
||||
/* ══════════════════════════════════════════════
|
||||
Champion Tier List
|
||||
══════════════════════════════════════════════ */
|
||||
|
||||
.lol-tier-section {
|
||||
margin-top: 24px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.lol-tier-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.lol-tier-modes {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.lol-tier-mode-btn {
|
||||
padding: 6px 14px;
|
||||
border: 1px solid var(--bg-tertiary);
|
||||
border-radius: 16px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.lol-tier-mode-btn:hover { border-color: var(--accent); color: var(--text-normal); }
|
||||
.lol-tier-mode-btn.active {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.lol-tier-filter {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid var(--bg-tertiary);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-normal);
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
width: 140px;
|
||||
margin-left: auto;
|
||||
}
|
||||
.lol-tier-filter:focus { border-color: var(--accent); }
|
||||
.lol-tier-filter::placeholder { color: var(--text-faint); }
|
||||
|
||||
/* Tier Table */
|
||||
.lol-tier-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.lol-tier-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--text-faint);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.lol-tier-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
background: var(--bg-secondary);
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.lol-tier-row:hover { background: var(--bg-tertiary); }
|
||||
|
||||
/* Tier row accent borders */
|
||||
.lol-tier-row.tier-0 { border-left: 3px solid #f4c874; }
|
||||
.lol-tier-row.tier-1 { border-left: 3px solid #d4a017; }
|
||||
.lol-tier-row.tier-2 { border-left: 3px solid #576cce; }
|
||||
.lol-tier-row.tier-3 { border-left: 3px solid #28b29e; }
|
||||
.lol-tier-row.tier-4 { border-left: 3px solid var(--bg-tertiary); }
|
||||
.lol-tier-row.tier-5 { border-left: 3px solid var(--bg-tertiary); opacity: 0.7; }
|
||||
|
||||
/* Column widths */
|
||||
.lol-tier-col-rank { width: 32px; text-align: center; font-weight: 600; flex-shrink: 0; }
|
||||
.lol-tier-col-champ {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
font-weight: 500;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
.lol-tier-col-champ img {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.lol-tier-col-tier { width: 36px; text-align: center; font-weight: 700; font-size: 12px; flex-shrink: 0; }
|
||||
.lol-tier-col-wr { width: 60px; text-align: center; flex-shrink: 0; }
|
||||
.lol-tier-col-pr { width: 60px; text-align: center; flex-shrink: 0; }
|
||||
.lol-tier-col-br { width: 60px; text-align: center; flex-shrink: 0; }
|
||||
.lol-tier-col-kda { width: 50px; text-align: center; flex-shrink: 0; }
|
||||
|
||||
/* Tier badge colors */
|
||||
.tier-badge-0 { color: #f4c874; }
|
||||
.tier-badge-1 { color: #d4a017; }
|
||||
.tier-badge-2 { color: #576cce; }
|
||||
.tier-badge-3 { color: #28b29e; }
|
||||
.tier-badge-4 { color: var(--text-faint); }
|
||||
.tier-badge-5 { color: var(--text-faint); }
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media (max-width: 640px) {
|
||||
.lol-search { flex-wrap: wrap; }
|
||||
|
|
@ -498,4 +625,8 @@
|
|||
.lol-match-meta { margin-left: 0; text-align: left; }
|
||||
.lol-match-items { flex-wrap: wrap; }
|
||||
.lol-profile { flex-wrap: wrap; }
|
||||
.lol-tier-controls { flex-direction: column; align-items: stretch; }
|
||||
.lol-tier-filter { width: 100%; margin-left: 0; }
|
||||
.lol-tier-col-br { display: none; }
|
||||
.lol-tier-col-kda { display: none; }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue