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>
204 lines
7.9 KiB
TypeScript
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>
|
|
);
|
|
}
|