aethex-forge/client/pages/StreamUpgrade.tsx
AeThex 2ae331f9fe fix: remove listGpus import/route breaking production build
listGpus was removed from session.ts when migrating to RunPod Serverless,
but server/index.ts still imported and registered it, failing the esbuild.
Also stages stream API, StreamUpgrade page, and Dockerfile/compose fixes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 16:42:44 +00:00

204 lines
7.9 KiB
TypeScript

import { useState, useEffect } from "react";
import { useSearchParams, useNavigate } from "react-router-dom";
import { Loader2, Zap, Users, Check } from "lucide-react";
import { useAuth } from "@/contexts/AuthContext";
import { supabase } from "@/lib/supabase";
import { aethexToast } from "@/lib/aethex-toast";
import Layout from "@/components/Layout";
const PLANS = [
{
id: "stream_pro",
name: "Stream Pro",
price: "$9.99",
period: "/mo",
icon: Zap,
seats: 1,
resolution: "1080p60",
bitrate: "15 Mbps",
features: ["GPU-accelerated stream", "1080p60 quality", "1 guaranteed seat", "Priority over free viewers", "No ads"],
},
{
id: "stream_team",
name: "Stream Team",
price: "$24.99",
period: "/mo",
icon: Users,
seats: 4,
resolution: "1080p60",
bitrate: "15 Mbps",
features: ["GPU-accelerated stream", "1080p60 quality", "4 guaranteed seats", "Priority over free viewers", "No ads", "Team management"],
highlighted: true,
},
] as const;
function StreamSuccess() {
const [token, setToken] = useState("");
useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => {
if (session?.access_token) setToken(session.access_token);
});
}, []);
return (
<Layout>
<div style={{ minHeight: "80vh", display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "'Courier New', monospace" }}>
<div style={{ textAlign: "center", maxWidth: 420, padding: "0 24px" }}>
<div style={{ fontSize: 48, marginBottom: 16 }}></div>
<div style={{ fontFamily: "Orbitron, monospace", fontSize: 20, fontWeight: 900, color: "#00FF88", letterSpacing: 3, marginBottom: 8 }}>
SUBSCRIPTION ACTIVE
</div>
<p style={{ color: "#8EA8CC", fontSize: 13, lineHeight: 1.7, marginBottom: 24 }}>
Your GPU stream access is now live. Head back to aethex.live to start playing.
</p>
<a
href={`https://aethex.live?token=${token}`}
style={{ display: "inline-block", padding: "10px 28px", border: "1px solid #00D4FF", background: "rgba(0,212,255,0.12)", color: "#00D4FF", borderRadius: 2, letterSpacing: 2, fontSize: 12, textDecoration: "none" }}
>
LAUNCH STREAM
</a>
</div>
</div>
</Layout>
);
}
export default function StreamUpgrade() {
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { user } = useAuth();
const [loading, setLoading] = useState<string | null>(null);
const defaultPlan = searchParams.get("plan") === "stream_team" ? "stream_team" : "stream_pro";
const [selectedPlan, setSelectedPlan] = useState<string>(defaultPlan);
async function handleCheckout() {
if (!user) {
navigate("/login?redirect=/stream/upgrade?plan=" + selectedPlan);
return;
}
setLoading(selectedPlan);
try {
const { data: { session } } = await supabase.auth.getSession();
const token = session?.access_token;
if (!token) throw new Error("Not authenticated");
const res = await fetch("/api/subscriptions/create-checkout", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
body: JSON.stringify({
tier: selectedPlan,
successUrl: `${window.location.origin}/stream/upgrade?success=true&plan=${selectedPlan}`,
cancelUrl: `${window.location.origin}/stream/upgrade?plan=${selectedPlan}`,
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Checkout failed");
// Pass token back to aethex.live after upgrade
if (data.url) {
window.location.href = data.url;
}
} catch (err: any) {
aethexToast.error(err.message || "Something went wrong");
} finally {
setLoading(null);
}
}
const success = searchParams.get("success") === "true";
if (success) {
return <StreamSuccess />;
}
return (
<Layout>
<div style={{ minHeight: "80vh", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", padding: "40px 24px", fontFamily: "'Courier New', monospace" }}>
<div style={{ textAlign: "center", marginBottom: 40 }}>
<div style={{ fontFamily: "Orbitron, monospace", fontSize: 22, fontWeight: 900, color: "#D8E8FF", letterSpacing: 4, marginBottom: 8 }}>
STREAM <span style={{ color: "#00D4FF" }}>ACCESS</span>
</div>
<p style={{ color: "#4E6280", fontSize: 13, maxWidth: 400, margin: "0 auto", lineHeight: 1.7 }}>
Upgrade for guaranteed GPU-accelerated seats on aethex.live. Free slots fill up fast.
</p>
</div>
<div style={{ display: "flex", gap: 16, marginBottom: 32, flexWrap: "wrap", justifyContent: "center" }}>
{PLANS.map((plan) => {
const Icon = plan.icon;
const isSelected = selectedPlan === plan.id;
return (
<div
key={plan.id}
onClick={() => setSelectedPlan(plan.id)}
style={{
width: 260,
border: `1px solid ${isSelected ? "#00D4FF" : plan.highlighted ? "#C9A84C" : "#161E38"}`,
borderRadius: 3,
padding: "24px 20px",
background: isSelected ? "rgba(0,212,255,0.06)" : "rgba(8,12,24,0.8)",
cursor: "pointer",
transition: "border-color .2s, background .2s",
position: "relative",
}}
>
{plan.highlighted && (
<div style={{ position: "absolute", top: -10, left: "50%", transform: "translateX(-50%)", background: "#C9A84C", color: "#04060E", fontSize: 9, padding: "3px 10px", borderRadius: 2, letterSpacing: 2, whiteSpace: "nowrap" }}>
BEST VALUE
</div>
)}
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
<Icon size={16} color={isSelected ? "#00D4FF" : "#4E6280"} />
<span style={{ fontFamily: "Orbitron, monospace", fontSize: 12, fontWeight: 700, color: "#D8E8FF", letterSpacing: 2 }}>
{plan.name.toUpperCase()}
</span>
</div>
<div style={{ marginBottom: 16 }}>
<span style={{ fontSize: 28, fontWeight: 700, color: "#00D4FF" }}>{plan.price}</span>
<span style={{ fontSize: 12, color: "#4E6280" }}>{plan.period}</span>
</div>
<ul style={{ listStyle: "none", padding: 0, margin: 0 }}>
{plan.features.map((f) => (
<li key={f} style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 11, color: "#8EA8CC", marginBottom: 6 }}>
<Check size={11} color="#00FF88" />
{f}
</li>
))}
</ul>
</div>
);
})}
</div>
<button
onClick={handleCheckout}
disabled={!!loading}
style={{
fontFamily: "'Courier New', monospace",
fontSize: 12,
padding: "12px 40px",
border: "1px solid #00D4FF",
background: "rgba(0,212,255,0.12)",
color: "#00D4FF",
cursor: loading ? "not-allowed" : "pointer",
borderRadius: 2,
letterSpacing: 2,
display: "flex",
alignItems: "center",
gap: 8,
opacity: loading ? 0.7 : 1,
}}
>
{loading ? <Loader2 size={14} className="animate-spin" /> : null}
{loading ? "REDIRECTING..." : "SUBSCRIBE NOW"}
</button>
<p style={{ marginTop: 12, fontSize: 10, color: "#364860" }}>
Cancel anytime · Secured by Stripe
</p>
</div>
</Layout>
);
}