Files
rehearshalhub/web/src/pages/InvitePage.tsx
2026-04-08 15:10:52 +02:00

106 lines
3.9 KiB
TypeScript
Executable File

import { useEffect, useState } from "react";
import { useParams, useNavigate } from "react-router-dom";
import { api, isLoggedIn } from "../api/client";
interface InviteInfo {
id: string;
band_id: string;
token: string;
role: string;
expires_at: string;
used_at: string | null;
}
export function InvitePage() {
const { token } = useParams<{ token: string }>();
const navigate = useNavigate();
const [invite, setInvite] = useState<InviteInfo | null>(null);
const [error, setError] = useState<string | null>(null);
const [accepting, setAccepting] = useState(false);
const [done, setDone] = useState(false);
const loggedIn = isLoggedIn();
useEffect(() => {
if (!token) return;
api.get<InviteInfo>(`/invites/${token}`)
.then(setInvite)
.catch((err) => setError(err instanceof Error ? err.message : "Invalid invite"));
}, [token]);
async function accept() {
if (!token) return;
setAccepting(true);
try {
await api.post(`/invites/${token}/accept`, {});
setDone(true);
setTimeout(() => navigate("/"), 2000);
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to accept invite");
} finally {
setAccepting(false);
}
}
function goLogin() {
navigate(`/login?next=/invite/${token}`);
}
return (
<div style={{ background: "var(--bg)", minHeight: "100vh", display: "flex", alignItems: "center", justifyContent: "center", color: "var(--text)" }}>
<div style={{ background: "var(--bg-subtle)", border: "1px solid var(--border)", borderRadius: 12, padding: 40, maxWidth: 420, width: "100%", textAlign: "center" }}>
<h1 style={{ color: "var(--accent)", fontFamily: "monospace", marginBottom: 8, fontSize: 22 }}> RehearsalHub</h1>
<p style={{ color: "var(--text-muted)", fontSize: 13, marginBottom: 28 }}>Band invite</p>
{error && (
<div style={{ background: "var(--danger-bg)", border: "1px solid var(--danger)", borderRadius: 6, padding: "12px 16px", color: "var(--danger)", fontSize: 13, marginBottom: 20 }}>
{error}
</div>
)}
{done && (
<div style={{ color: "var(--teal)", fontSize: 14 }}>
Joined! Redirecting
</div>
)}
{!done && invite && (
<>
<p style={{ color: "var(--text)", fontSize: 15, marginBottom: 6 }}>
You've been invited to join a band as <strong style={{ color: "var(--accent)" }}>{invite.role}</strong>.
</p>
<p style={{ color: "var(--text-muted)", fontSize: 12, marginBottom: 28 }}>
Expires {new Date(invite.expires_at).toLocaleDateString()}
{invite.used_at && " · Already used"}
</p>
{loggedIn ? (
<button
onClick={accept}
disabled={accepting || !!invite.used_at}
style={{ width: "100%", background: "var(--accent)", border: "none", borderRadius: 8, color: "var(--accent-fg)", cursor: "pointer", padding: "12px 0", fontWeight: 700, fontSize: 15 }}
>
{accepting ? "Joining…" : "Accept Invite"}
</button>
) : (
<div>
<p style={{ color: "var(--text-muted)", fontSize: 13, marginBottom: 16 }}>Log in or register to accept this invite.</p>
<button
onClick={goLogin}
style={{ width: "100%", background: "var(--accent)", border: "none", borderRadius: 8, color: "var(--accent-fg)", cursor: "pointer", padding: "12px 0", fontWeight: 700, fontSize: 15 }}
>
Log in / Register
</button>
</div>
)}
</>
)}
{!done && !invite && !error && (
<p style={{ color: "var(--text-muted)" }}>Loading invite</p>
)}
</div>
</div>
);
}