Skip to content

Commit bc52dda

Browse files
committed
show twitch stream and update LP
1 parent 40ff44b commit bc52dda

4 files changed

Lines changed: 84 additions & 15 deletions

File tree

resources/js/components/race/leaderboard-row.tsx

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Link } from '@inertiajs/react';
22
import { motion } from 'framer-motion';
33
import { useState } from 'react';
4-
import { useCountup } from '@/hooks/use-countup';
54
import { tierColor, tierLabel } from '@/lib/tier-utils';
65
import type { LeaderboardRow as RowType, MatchFeedRow } from '@/types/race';
76
import { MatchDrawer } from './match-drawer';
@@ -19,7 +18,6 @@ export const rowVariant = {
1918

2019
export function LeaderboardRow({ row, position, streamerMatches }: Props) {
2120
const [expanded, setExpanded] = useState(false);
22-
const lp = useCountup(row.total_lp);
2321
const isTop = position === 1;
2422

2523
return (
@@ -41,7 +39,7 @@ export function LeaderboardRow({ row, position, streamerMatches }: Props) {
4139
</span>
4240

4341
{/* Rank badge */}
44-
<TierBadge tier={row.tier} rank={row.rank} />
42+
<TierBadge tier={row.tier} rank={row.rank} points={row.points} />
4543

4644
{/* Name & account */}
4745
<div className="flex-1 min-w-0">
@@ -54,14 +52,6 @@ export function LeaderboardRow({ row, position, streamerMatches }: Props) {
5452
<p className="text-xs text-[#4a4a60] truncate">{row.account_display_name}</p>
5553
</div>
5654

57-
{/* Total LP */}
58-
<div className="text-right hidden sm:block flex-shrink-0">
59-
<span className="text-base font-mono font-bold text-cyan-300">
60-
{lp.toLocaleString()}
61-
</span>
62-
<p className="text-xs text-[#4a4a60]">Total LP</p>
63-
</div>
64-
6555
{/* W / L */}
6656
<div className="text-right hidden md:block flex-shrink-0">
6757
<p className="text-sm font-mono">
@@ -98,7 +88,7 @@ export function LeaderboardRow({ row, position, streamerMatches }: Props) {
9888
);
9989
}
10090

101-
function TierBadge({ tier, rank }: { tier: string | null; rank: string | null }) {
91+
function TierBadge({ tier, rank, points }: { tier: string | null; rank: string | null; points: number | null }) {
10292
if (!tier) {
10393
return (
10494
<span className="px-2 py-0.5 rounded text-xs font-semibold bg-white/5 text-[#4a4a60] flex-shrink-0">
@@ -114,7 +104,7 @@ function TierBadge({ tier, rank }: { tier: string | null; rank: string | null })
114104
transition={{ duration: 0.2 }}
115105
className={`px-2 py-0.5 rounded text-xs font-semibold flex-shrink-0 ${tierColor(tier)}`}
116106
>
117-
{tierLabel(tier)} {rank}
107+
{tierLabel(tier)} {rank}{points !== null ? ` ${points} LP` : ''}
118108
</motion.span>
119109
);
120110
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { useMemo } from 'react';
2+
3+
function extractTwitchChannel(url: string): string | null {
4+
try {
5+
const u = new URL(url);
6+
if (u.hostname === 'twitch.tv' || u.hostname === 'www.twitch.tv') {
7+
const channel = u.pathname.replace(/^\//, '').split('/')[0];
8+
return channel || null;
9+
}
10+
} catch {
11+
// invalid URL
12+
}
13+
return null;
14+
}
15+
16+
interface Props {
17+
url: string;
18+
}
19+
20+
export function TwitchEmbed({ url }: Props) {
21+
const channel = extractTwitchChannel(url);
22+
const parent = useMemo(
23+
() => (typeof window !== 'undefined' ? window.location.hostname : 'localhost'),
24+
[],
25+
);
26+
27+
if (!channel) return null;
28+
29+
return (
30+
<div className="aspect-video w-full rounded-lg overflow-hidden border border-white/10">
31+
<iframe
32+
src={`https://player.twitch.tv/?channel=${channel}&parent=${parent}`}
33+
className="w-full h-full"
34+
allowFullScreen
35+
/>
36+
</div>
37+
);
38+
}

resources/js/pages/home.tsx

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import { Head } from '@inertiajs/react';
2-
import moment from 'moment-timezone';
32
import { motion } from 'framer-motion';
3+
import moment from 'moment-timezone';
4+
import { useMemo, useState } from 'react';
45
import { Leaderboard } from '@/components/race/leaderboard';
56
import { LpChart } from '@/components/race/lp-chart';
67
import { MatchFeed } from '@/components/race/match-feed';
78
import { RaceHeader } from '@/components/race/race-header';
9+
import { TwitchEmbed } from '@/components/twitch-embed';
810
import { useCountdown } from '@/hooks/use-countdown';
911
import PublicLayout from '@/layouts/public-layout';
10-
import type { RaceData } from '@/types/race';
12+
import type { LeaderboardRow, RaceData } from '@/types/race';
1113

1214
interface UpcomingStreamer {
1315
id: number;
@@ -47,12 +49,49 @@ export default function Home({ race, upcoming, last }: Props) {
4749
}
4850

4951
function RaceView({ race, isLast = false }: { race: RaceData; isLast?: boolean }) {
52+
const streamers = useMemo(
53+
() => race.leaderboard.filter((r) => !!r.stream_url),
54+
[race.leaderboard],
55+
);
56+
const [activeStream, setActiveStream] = useState<LeaderboardRow | null>(() => {
57+
if (race.stream_url) return null; // handled separately
58+
if (streamers.length === 0) return null;
59+
return streamers[Math.floor(Math.random() * streamers.length)];
60+
});
61+
62+
const embedUrl = race.stream_url ?? activeStream?.stream_url ?? null;
63+
5064
return (
5165
<div>
5266
<RaceHeader race={race} isLast={isLast} />
5367
<div className="mx-auto max-w-5xl px-4 pb-24 pt-4 space-y-10">
5468
<SponsorBanner />
69+
5570
<Leaderboard leaderboard={race.leaderboard} matches={race.matches} />
71+
72+
{embedUrl && (
73+
<div className="space-y-0">
74+
{/* Tabs — only shown when there is no dedicated race stream */}
75+
{!race.stream_url && streamers.length > 1 && (
76+
<div className="flex gap-1 mb-2 flex-wrap">
77+
{streamers.map((s) => (
78+
<button
79+
key={s.streamer_id}
80+
onClick={() => setActiveStream(s)}
81+
className={`px-3 py-1 rounded text-xs font-semibold transition-colors ${
82+
activeStream?.streamer_id === s.streamer_id
83+
? 'bg-cyan-500/20 text-cyan-300 border border-cyan-500/40'
84+
: 'bg-white/5 text-[#4a4a60] border border-white/5 hover:text-white hover:border-white/20'
85+
}`}
86+
>
87+
{s.name}
88+
</button>
89+
))}
90+
</div>
91+
)}
92+
<TwitchEmbed url={embedUrl} />
93+
</div>
94+
)}
5695
<LpChart series={race.lp_series} />
5796
<MatchFeed matches={race.matches} />
5897
</div>

resources/js/pages/streamer.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Head } from '@inertiajs/react';
22
import { LpHistoryChart } from '@/components/streamer/lp-history-chart';
33
import { MatchList } from '@/components/streamer/match-list';
44
import { StreamerHero } from '@/components/streamer/streamer-hero';
5+
import { TwitchEmbed } from '@/components/twitch-embed';
56
import PublicLayout from '@/layouts/public-layout';
67
import type { LpPoint, MatchFeedRow } from '@/types/race';
78
import type { StreamerProfile } from '@/types/streamer';
@@ -21,6 +22,7 @@ export default function Streamer({ streamer, recent_matches, lp_history }: Props
2122
<div>
2223
<StreamerHero streamer={streamer} championIconUrl={championIconUrl} />
2324
<div className="mx-auto max-w-5xl px-4 py-10 space-y-10">
25+
{streamer.stream_url && <TwitchEmbed url={streamer.stream_url} />}
2426
<LpHistoryChart history={lp_history} />
2527
<MatchList matches={recent_matches} />
2628
</div>

0 commit comments

Comments
 (0)